OMP Workbook

Read the source. Follow the evidence.

Background work and owner-addressed delivery

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

“When the promise finishes, call sendMessage().”

That sends to the runtime’s current session, not necessarily the original owner.

Mina separates work lifetime, result storage, delivery admission and agent wake-up.

Milestone: managed timers for a local reminder

Start with a small timer that only notifies the operator. It does not pretend to be a durable job.

Complete TypeScript exercise—save as clock-note.ts:

Background work and owner-addressed delivery · source excerpt 1; read surrounding instructions
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:

Background work and owner-addressed delivery · source excerpt 2; read surrounding instructions
/clock-note start
/clock-note stop

Expected checkpoint: start produces one reminder if the host stays alive long enough; 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, so they do not keep the process alive alone;
  • are cleared on shutdown;
  • can be cleared individually with clearTimer().

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

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

Exercise: remove the explicit pre-switch clear and start a reminder before /new.

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

Ordinary messages: choose the effect deliberately

APIIntended useImportant semantics
appendEntry(type, data)Extension stateNot sent to the model
sendMessage(payload, options)Custom conversation/context messagecontent participates in model context; details is metadata
sendUserMessage(content, options)User-style prompt or queued user messageDoes not dispatch slash commands or expand prompt templates
captureSessionTarget() + deliverMessage()Owner-addressed result deliveryPersistent anchor, admission checks and deduplication receipt

sendMessage() and sendUserMessage() return void through ExtensionAPI. Awaiting them does not produce a durable acknowledgement.

For custom messages:

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

For sendUserMessage():

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

display: false hides presentation, not model visibility.

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

Milestone: capture once, deliver to that owner

This complete exercise demonstrates immediate capture/delivery and a retained retry identity. 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 triggerTurn is false. Without a model/capacity snapshot, context_full can be a correct deferred result.

Complete TypeScript exercise—save as owner-note.ts:

Background work and owner-addressed delivery · source excerpt 3; read surrounding instructions
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:

Background work and owner-addressed delivery · source excerpt 4; read surrounding instructions
/owner-note new The fictional marsh check is complete.
/owner-note status
/owner-note retry

Use retry only if the previous operation retained a pending item. A successfully committed item is cleared by this small example.

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

Capture contract

captureSessionTarget({ namespace, requestId, data? }) appends an anchor and returns:

  • sessionId;
  • sessionFile;
  • anchorEntryId.

Identifiers must be nonempty. data must be structured-cloneable. Capture requires persistence and can fail during a conflicting transition.

Capture once before launching work. 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 }) returns:

Receipt fieldMeaning
deliveryIdYour 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; no duplicate body
entryIdCommitted journal entry, when available
reasonAdmission or wake condition
wakenot_scheduled, scheduled or pending

Reasons are:

  • owner_inactive;
  • branch_inactive;
  • session_busy;
  • context_full;
  • host_defers_turn.

Inspect state and wake separately. 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. A fork creates a new session ID and does not inherit delivery authority. Navigating away from the anchor or crossing a reset boundary can make the branch inactive.

A namespace/delivery-ID retry with different content, owner or anchor throws a conflict. The hash represents typed content segments, not every presentation field in the payload; 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. The coordinator does not truncate or compact the result to make it fit.

details["omp.delivery"] is reserved for delivery identity.

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

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

What changes next?

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

A useful new tool should then expose:

  • a stable job ID;
  • inspect/query for state;
  • a bounded, cancellable wait;
  • diagnose for a deferred owner, unavailable service or pending wake;
  • explicit action retry rules.

Independent processes writing the same transcript need an external owner lock. Delivery serialization is session-local.

Exercise: a receipt says state: "committed", reason: "host_defers_turn", wake: "pending". Should the worker resend under a new delivery ID?

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

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

Extensions inside those boundaries · Source chapter: extensions/background-work-and-owner-addressed-delivery. Original evidence remains scoped to its recorded snapshot.

Read this chapter as Markdown

Your lesson ticks

A self-reported reading checklist, not proof of real OMP behavior. Only these ticks are saved in this browser. Reading a milestone does not resume, fork, reset or export a session.

Chapters I have worked through
Start here 1
Sessions, resets, and reviewable history 19
Memory and reusable knowledge 14
Tangent work and live control 17
Tool permissions and approvals 15
Extensions inside those boundaries 23
Connections and next steps 8
0 of 97 checked

Checklist saving needs JavaScript and available browser storage.