## Background work and owner\-addressed delivery

Mina starts a fictional habitat check\,&#32;then moves to another conversation before the result arrives\.&#32;Her goal is to keep the result with its origin\.&#32;Her obstacle is a tempting implementation\:

> “When the promise finishes\,&#32;call&#32;`sendMessage()`\.”

That sends to the runtime’s current session\,&#32;not necessarily the original owner\.

Mina separates&#32;**work lifetime**\,&#32;**result storage**\,&#32;**delivery admission**&#32;and&#32;**agent wake\-up**\.

### Milestone\:&#32;managed timers for a local reminder

Start with a small timer that only notifies the operator\.&#32;It does not pretend to be a durable job\.

**Complete TypeScript exercise—save as&#32;`clock-note.ts`\:**

~~~ts
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";

export default function clockNote(pi: ExtensionAPI): void {
    let timer: Timer | undefined;

    function clear(ctx: ExtensionContext): void {
        if (timer !== undefined) ctx.clearTimer(timer);
        timer = undefined;
    }

    pi.registerCommand("clock-note", {
        description: "Schedule or stop a one-second local reminder",
        async handler(args, ctx) {
            clear(ctx);
            if (args.trim() === "stop") {
                ctx.ui.notify("Reminder stopped.");
                return;
            }
            if (args.trim() && args.trim() !== "start") {
                ctx.ui.notify("Use /clock-note start or /clock-note stop.", "warning");
                return;
            }
            const owner = ctx.sessionManager.getSessionId();
            timer = ctx.setTimeout(() => {
                timer = undefined;
                if (ctx.sessionManager.getSessionId() !== owner) return;
                pi.logger.debug("Workbook reminder fired");
                if (ctx.hasUI) ctx.ui.notify("One-second workbook reminder.");
            }, 1000);
            ctx.ui.notify("Reminder scheduled.");
        },
    });

    pi.on("session_before_switch", (_event, ctx) => clear(ctx));
    pi.on("session_before_branch", (_event, ctx) => clear(ctx));
    pi.on("session_before_tree", (_event, ctx) => clear(ctx));
    pi.on("session_shutdown", (_event, ctx) => clear(ctx));
}
~~~

**Human OMP slash commands—after loading\:**

~~~text
/clock-note start
/clock-note stop
~~~

**Expected checkpoint\:**&#32;start produces one reminder if the host stays alive long enough\;&#32;stop or a navigation attempt clears it\.

Managed timers\:

- contain synchronous callback throws and rejected native promises\;
- report failures through the extension error channel\;
- are unref’d\,&#32;so they do not keep the process alive alone\;
- are cleared on shutdown\;
- can be cleared individually with&#32;`clearTimer()`\.

Clearing a timer does not cancel asynchronous work that its callback already started\.&#32;That work needs its own abort controller and stale\-result checks\.

Managed timers do not automatically register jobs in&#32;`getAsyncJobSnapshot()`\.&#32;That snapshot reports the host’s session\-owned async jobs\,&#32;or null when no applicable manager is available\.

**Exercise\:**&#32;remove the explicit pre\-switch clear and start a reminder before&#32;`/new`\.

**Answer\:**&#32;the runner\/binding can survive&#32;`/new`\;&#32;automatic shutdown cleanup is not a session\-switch reset\.&#32;Design the intended lifetime explicitly\.

### Ordinary messages\:&#32;choose the effect deliberately

| API | Intended use | Important semantics |
| --- | --- | --- |
| `appendEntry(type, data)` | Extension state | Not sent to the model |
| `sendMessage(payload, options)` | Custom conversation\/context message | `content`&#32;participates in model context\;&#32;`details`&#32;is metadata |
| `sendUserMessage(content, options)` | User\-style prompt or queued user message | Does not dispatch slash commands or expand prompt templates |
| `captureSessionTarget()`&#32;\+&#32;`deliverMessage()` | Owner\-addressed result delivery | Persistent anchor\,&#32;admission checks and deduplication receipt |

`sendMessage()`&#32;and&#32;`sendUserMessage()`&#32;return&#32;`void`&#32;through&#32;`ExtensionAPI`\.&#32;Awaiting them does not produce a durable acknowledgement\.

For custom messages\:

- Idle\,&#32;no trigger\:&#32;append to conversation\/session without starting a turn\.
- Streaming\,&#32;default delivery\:&#32;queue as steering\.
- Streaming\,&#32;`followUp`\:&#32;queue behind the current work\.
- Streaming\,&#32;`nextTurn`\:&#32;hold as hidden next\-turn context rather than an editable pending\-message chip\.
- `triggerTurn: true`\:&#32;request a turn\;&#32;hosts may defer it\.
- Idle&#32;`nextTurn`&#32;without a trigger appends immediately in this implementation\;&#32;it is not a separate durable outbox\.

For&#32;`sendUserMessage()`\:

- omitted&#32;`deliverAs`&#32;starts prompt flow when idle and steers while streaming\;
- explicit&#32;`steer`&#32;or&#32;`followUp`&#32;enqueues through that queue\,&#32;without synchronously starting a prompt\;
- host queue\-drain behavior may subsequently resume work\;
- **`nextTurn`&#32;is not an option on this API\.**

`display: false`&#32;hides presentation\,&#32;not model visibility\.

The old repository reload\-tool example queues&#32;`"/reload-runtime"`&#32;through&#32;`sendUserMessage()`\.&#32;Current&#32;`AgentSession.sendUserMessage()`&#32;explicitly skips command handling\.&#32;Do not teach that as a supported way for a model to invoke a human command\.

### Milestone\:&#32;capture once\,&#32;deliver to that owner

This complete exercise demonstrates immediate capture\/delivery and a retained retry identity\.&#32;It is not a durable background job engine\.

It requires\:

- an initialized extension runtime\;
- a persistent session\;
- enough known model context capacity for admission\.

No inference is requested because&#32;`triggerTurn`&#32;is false\.&#32;Without a model\/capacity snapshot\,&#32;`context_full`&#32;can be a correct deferred result\.

**Complete TypeScript exercise—save as&#32;`owner-note.ts`\:**

~~~ts
import type { ExtensionAPI, ExtensionSessionTarget } from "@oh-my-pi/pi-coding-agent";

export default function ownerNote(pi: ExtensionAPI): void {
    const namespace = "workbook.owner-note";
    let pending: {
        target: ExtensionSessionTarget;
        deliveryId: string;
        content: string;
    } | undefined;
    let busy = false;

    pi.registerCommand("owner-note", {
        description: "Capture and deliver a local note: new <text> | retry | status",
        async handler(args, ctx) {
            const input = args.trim();
            if (input === "status") {
                ctx.ui.notify(pending ? `Pending delivery: ${pending.deliveryId}` : "No pending delivery.");
                return;
            }
            if (busy) throw new Error("A delivery operation is already in progress.");
            busy = true;
            try {
                if (input.startsWith("new ")) {
                    if (pending) throw new Error("Retry the existing pending delivery before creating another.");
                    const content = input.slice(4).trim();
                    if (!content) throw new Error("A nonblank note is required.");
                    const requestId = crypto.randomUUID();
                    const target = await pi.captureSessionTarget({
                        namespace,
                        requestId,
                        data: { kind: "fictional-workbook-note" },
                    });
                    pending = { target, deliveryId: requestId, content };
                } else if (input !== "retry") {
                    throw new Error("Use new <text>, retry, or status.");
                }

                if (!pending) throw new Error("No captured note is waiting for delivery.");
                const receipt = await pi.deliverMessage(
                    { customType: namespace, content: pending.content, display: true },
                    {
                        target: pending.target,
                        namespace,
                        deliveryId: pending.deliveryId,
                        triggerTurn: false,
                    },
                );
                ctx.ui.notify(
                    `${receipt.state}; reason=${receipt.reason ?? "none"}; wake=${receipt.wake}`,
                    receipt.state === "deferred" ? "warning" : "info",
                );
                if (receipt.state !== "deferred") pending = undefined;
            } finally {
                busy = false;
            }
        },
    });
}
~~~

**Human OMP slash commands—after loading with a persistent session\:**

~~~text
/owner-note new The fictional marsh check is complete.
/owner-note status
/owner-note retry
~~~

Use&#32;`retry`&#32;only if the previous operation retained a pending item\.&#32;A successfully committed item is cleared by this small example\.

The note’s retry state is binding\-local\.&#32;If the process exits before delivery\,&#32;this exercise has no durable outbox from which to recover it\.&#32;A real background system must persist the target\,&#32;stable delivery ID and complete body with its job\.

#### Capture contract

`captureSessionTarget({ namespace, requestId, data? })`&#32;appends an anchor and returns\:

- `sessionId`\;
- `sessionFile`\;
- `anchorEntryId`\.

Identifiers must be nonempty\.&#32;`data`&#32;must be structured\-cloneable\.&#32;Capture requires persistence and can fail during a conflicting transition\.

Capture once before launching work\.&#32;Repeated calls with the same request ID are not an instruction to find and reuse an earlier anchor\.

#### Receipt contract

`deliverMessage(payload, { target, namespace, deliveryId, triggerTurn })`&#32;returns\:

| Receipt field | Meaning |
| --- | --- |
| `deliveryId` | Your stable delivery identity |
| `state: "deferred"` | No new body appended by this attempt |
| `state: "committed"` | Body persisted and published to the owner’s live context |
| `state: "already_committed"` | Existing entry reused\;&#32;no duplicate body |
| `entryId` | Committed journal entry\,&#32;when available |
| `reason` | Admission or wake condition |
| `wake` | `not_scheduled`\,&#32;`scheduled`&#32;or&#32;`pending` |

Reasons are\:

- `owner_inactive`\;
- `branch_inactive`\;
- `session_busy`\;
- `context_full`\;
- `host_defers_turn`\.

Inspect state and wake separately\.&#32;A committed body can still have a pending wake\.

A scheduled wake is not proof that the model has replied\.

#### Ownership and deduplication

The coordinator checks\:

- the original session identity and file lineage\;
- the anchor on the active branch\;
- reset boundaries\;
- busy session work\;
- context capacity\.

A recorded move can preserve owner identity\.&#32;A fork creates a new session ID and does not inherit delivery authority\.&#32;Navigating away from the anchor or crossing a reset boundary can make the branch inactive\.

A namespace\/delivery\-ID retry with different content\,&#32;owner or anchor throws a conflict\.&#32;The hash represents typed content segments\,&#32;not every presentation field in the payload\;&#32;keep the entire payload stable rather than expecting changed details to update an existing delivery\.

Text is split into blocks of at most 65\,536 UTF\-16 code units without splitting surrogate pairs\.&#32;The coordinator does not truncate or compact the result to make it fit\.

`details["omp.delivery"]`&#32;is reserved for delivery identity\.

Already\-compacted messages are not resurrected\.&#32;Retrying a delivery can repair a pending\/failed wake without adding another body\,&#32;but a later successful assistant entry suppresses another wake\.

**Observed contract tests\:**&#32;real session persistence and coordinator scenarios covered duplicate delivery\,&#32;conflict\,&#32;failed persistence\,&#32;owner transitions\,&#32;large complete bodies and wake retry\.&#32;Some lifecycle tests used a mock model to test the agent loop\.&#32;That is not live\-provider evidence\.

### What changes next\?

For a real service job\,&#32;Mina needs an additional durable job\/outbox store and retry policy\.&#32;No public&#32;`ExtensionAPI.startJob()`&#32;or magic shared durable backend exists in this inventory\.

A useful new tool should then expose\:

- a stable job ID\;
- `inspect/query`&#32;for state\;
- a bounded\,&#32;cancellable&#32;`wait`\;
- `diagnose`&#32;for a deferred owner\,&#32;unavailable service or pending wake\;
- explicit action retry rules\.

Independent processes writing the same transcript need an external owner lock\.&#32;Delivery serialization is session\-local\.

**Exercise\:**&#32;a receipt says&#32;`state: "committed"`\,&#32;`reason: "host_defers_turn"`\,&#32;`wake: "pending"`\.&#32;Should the worker resend under a new delivery ID\?

**Answer\:**&#32;no\.&#32;Retain the same identity\.&#32;The body is already committed\;&#32;the outstanding issue is consumption\,&#32;not missing storage\.

*Source\,&#32;snapshot 2026\-08\-29\:&#32;`packages/coding-agent/src/extensibility/extensions/managed-timers.ts`\;&#32;`packages/coding-agent/src/session/extension-delivery.ts`\,&#32;`ExtensionDeliveryCoordinator`\;&#32;`packages/coding-agent/src/session/agent-session.ts`\,&#32;message APIs and async snapshots\;&#32;`packages/coding-agent/test/extension-delivery.test.ts`\,&#32;`extension-delivery-lifecycle.test.ts`\.*
