OMP Workbook

Read the source. Follow the evidence.

Session navigation and event-driven behavior

Lena works through a long fictional review. She wants a checkpoint command that can inspect the current session and move to a deliberate place in its history.

Her obstacle is lifecycle timing: a tool executes inside an agent operation, while session navigation may need to abort, restore history or wait for the operation to settle.

She keeps navigation on a command context.

Milestone: explicit session controls

The following is a complete source-backed exercise. It is not a new Seed Desk or Review Desk stage.

Complete TypeScript exercise—save as session-desk.ts:

Session navigation and event-driven behavior · source excerpt 1; read surrounding instructions
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

export default function sessionDesk(pi: ExtensionAPI): void {
    pi.registerCommand("workbook-session", {
        description: "Session controls: status | new | reload | compact | tree <id> | branch <id> | switch <file>",
        async handler(args, ctx) {
            const trimmed = args.trim();
            const split = trimmed.indexOf(" ");
            const verb = split < 0 ? trimmed : trimmed.slice(0, split);
            const target = split < 0 ? "" : trimmed.slice(split + 1).trim();

            if (!verb || verb === "status") {
                const current = ctx.models.current();
                const details = {
                    sessionId: ctx.sessionManager.getSessionId(),
                    sessionFile: ctx.sessionManager.getSessionFile() ?? null,
                    leafId: ctx.sessionManager.getLeafId(),
                    cwd: ctx.cwd,
                    model: current ? `${current.provider}/${current.id}` : null,
                    contextUsage: ctx.getContextUsage() ?? null,
                    asyncJobs: ctx.getAsyncJobSnapshot(),
                    pendingMessages: ctx.hasPendingMessages(),
                };
                pi.sendMessage(
                    { customType: "workbook.session-status", content: JSON.stringify(details), details, display: true },
                    { triggerTurn: false },
                );
                return;
            }

            await ctx.waitForIdle();

            if (verb === "reload") {
                await ctx.reload();
                return;
            }
            if (verb === "compact") {
                await ctx.compact("Preserve concrete decisions, unresolved questions and stable domain IDs.");
                return;
            }

            let result: { cancelled: boolean };
            if (verb === "new") {
                result = await ctx.newSession();
            } else if (verb === "tree" && ctx.sessionManager.getEntry(target)) {
                result = await ctx.navigateTree(target, { summarize: false });
            } else if (verb === "branch") {
                const entry = ctx.sessionManager.getEntry(target);
                if (entry?.type !== "message" || entry.message.role !== "user") {
                    throw new Error("branch requires an existing user-message entry ID.");
                }
                result = await ctx.branch(target);
            } else if (verb === "switch" && target) {
                if (!(await Bun.file(target).exists())) {
                    throw new Error("The target session file does not exist.");
                }
                result = await ctx.switchSession(target);
            } else {
                throw new Error("Use status, new, reload, compact, tree <id>, branch <id>, or switch <file>.");
            }
            ctx.ui.notify(result.cancelled ? "Session operation cancelled." : "Session operation completed.");
        },
    });
}

Human OMP slash command—after loading the exercise:

Session navigation and event-driven behavior · source excerpt 2; read surrounding instructions
/workbook-session status

Use actual entry IDs from the current session manager/tree, not a workbook placeholder, for navigation.

The status command is a snapshot. It also appends a custom message, so its displayed leafId is the position just before that status message was appended.

What the controls mean

  • waitForIdle() waits through the host’s wired idle operation. It is not an exclusive lock against another caller starting work.
  • newSession() starts a fresh transcript and returns { cancelled }.
  • newSession({ parentSession, setup }) can associate a parent and run an initialization callback. That callback receives the full session manager for deliberate setup.
  • branch(entryId) uses a user-message entry to create a new session file from its preceding history.
  • navigateTree(targetId, { summarize }) changes the current path in the same file.
  • switchSession(path) restores another session.
  • reload() reopens the current session, as discussed earlier.
  • compact() requests context compaction.

A user-message tree target normally moves to its parent and offers that message for editing. A non-user target generally lands on the selected node. Do not equate every target ID with “include this entry as the new leaf.”

Expected checkpoint: navigating before a Seed Desk reservation changes the query result because that extension reconstructs the current branch. Field Notes’ binding-local selection does not change merely because the transcript moves.

Failure boundary: pre-navigation handlers may cancel. Missing/invalid targets fail. Summarization and compaction may need a configured model/provider and may incur costs. The exercise uses summarize: false for ordinary tree navigation.

Exercise: call ctx.newSession() from a tool by casting its context.

Answer: do not. General tool/event contexts intentionally omit these command-only navigation methods. A cast does not make the lifecycle operation safe.

Compaction is not domain persistence

Compaction changes the model’s retained conversational context. It is not a replacement for reconstructing extension state from journal entries.

The supported compaction surfaces are:

  • session_before_compact: cancel or supply a complete custom compaction result;
  • session.compacting: add summary context, replace the summarizer prompt or attach preserveData;
  • session_compact: observe the resulting entry.

CompactOptions contains:

  • onComplete;
  • onError;
  • mode;
  • internalGuidance.

A string passed to ctx.compact() is public custom instructions. An options object can request a one-off supported compaction mode. The supplied type commentary names soft, remote and snapcompact; use the matching host’s CompactMode rather than inventing another value.

internalGuidance is for native summarizer guidance, not the user’s customInstructions visible to the pre-compaction hook.

The general context also exposes compact(), but lifecycle-sensitive compaction is best driven from a command or a host-controlled safe boundary. Awaiting maintenance from inside work that maintenance itself must drain can create a deadlock.

Change a prompt through the supported return shape

Lena wants a temporary instruction to label fictional output, without permanently editing the base prompt.

Complete TypeScript exercise—save as fictional-turn.ts:

Session navigation and event-driven behavior · source excerpt 3; read surrounding instructions
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

export default function fictionalTurn(pi: ExtensionAPI): void {
    pi.on("before_agent_start", event => ({
        systemPrompt: [
            ...event.systemPrompt,
            "Clearly distinguish fictional workbook data from real observations.",
        ],
    }));
}

This returns the complete replacement block array for that turn, preserving existing blocks.

There is no current native systemPromptAppend result member. The older pirate example uses that stale name; do not copy it as a working contract.

context is the per-model-call message transformation surface. Return new arrays and objects. The runner attempts a structured clone, but falls back to a shallow array copy for noncloneable data; in-place mutation is therefore a poor portability strategy.

Never remove one half of a tool-call/tool-result pair casually.

Handler timing and errors

Most event handlers run in extension order with a 30-second budget.

Important exceptions and qualifications:

  • tool_call uses extensionHandlers.toolCallTimeoutMs, defaulting to 30 seconds when unset or invalid.
  • tool_call errors/timeouts block execution.
  • Its budget pauses while awaiting supported human dialogs; asynchronous custom-component setup still consumes budget until the component is ready.
  • Other event handlers do not inherit that same paused human-dialog budget.
  • session_shutdown handlers run concurrently with a two-second per-handler cap.
  • Factory loading and command handlers are not automatically covered by that event-handler budget.
  • Most other event-handler failures are reported and dispatch continues. A failed pre-switch handler is not automatically a veto.
  • A timeout cannot preempt synchronous JavaScript or magically cancel an arbitrary detached promise. Code must honor cancellation and retire stale work.

message_start, message_update and message_end are notifications. message_end receives a detached snapshot; changing it does not rewrite the next provider request.

agent_end is also notification-only. It may indicate an already scheduled continuation through willContinue. Use session_stop for a supported stop-time continuation request, not an assumed return value from agent_end.

Exercise: throw from user_bash to prohibit a shell command.

Answer: that is not a reliable veto. Its handler errors are isolated, and the default path can continue. Use a supported interception result or an actual enforcement boundary appropriate to the operation.

Source, snapshot 2026-08-29: packages/coding-agent/src/session/agent-session.ts, session navigation and event mapping; packages/coding-agent/src/extensibility/extensions/runner.ts, #runHandlerWithTimeout, emit, emitBeforeAgentStart; packages/coding-agent/src/extensibility/shared-events.ts; packages/coding-agent/src/extensibility/extensions/compact-handler.ts.

Extensions inside those boundaries · Source chapter: extensions/session-navigation-and-event-driven-behavior. 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.