## Session navigation and event\-driven behavior

Lena works through a long fictional review\.&#32;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\:&#32;a tool executes inside an agent operation\,&#32;while session navigation may need to abort\,&#32;restore history or wait for the operation to settle\.

She keeps navigation on a command context\.

### Milestone\:&#32;explicit session controls

The following is a complete source\-backed exercise\.&#32;It is not a new Seed Desk or Review Desk stage\.

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

~~~ts
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\:**

~~~text
/workbook-session status
~~~

Use actual entry IDs from the current session manager\/tree\,&#32;not a workbook placeholder\,&#32;for navigation\.

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

#### What the controls mean

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

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

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

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

**Exercise\:**&#32;call&#32;`ctx.newSession()`&#32;from a tool by casting its context\.

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

### Compaction is not domain persistence

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

The supported compaction surfaces are\:

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

`CompactOptions`&#32;contains\:

- `onComplete`\;
- `onError`\;
- `mode`\;
- `internalGuidance`\.

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

`internalGuidance`&#32;is for native summarizer guidance\,&#32;not the user’s&#32;`customInstructions`&#32;visible to the pre\-compaction hook\.

The general context also exposes&#32;`compact()`\,&#32;but lifecycle\-sensitive compaction is best driven from a command or a host\-controlled safe boundary\.&#32;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\,&#32;without permanently editing the base prompt\.

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

~~~ts
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\,&#32;preserving existing blocks\.

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

`context`&#32;is the per\-model\-call message transformation surface\.&#32;Return new arrays and objects\.&#32;The runner attempts a structured clone\,&#32;but falls back to a shallow array copy for noncloneable data\;&#32;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&#32;**30\-second**&#32;budget\.

Important exceptions and qualifications\:

- `tool_call`&#32;uses&#32;`extensionHandlers.toolCallTimeoutMs`\,&#32;defaulting to 30 seconds when unset or invalid\.
- `tool_call`&#32;errors\/timeouts block execution\.
- Its budget pauses while awaiting supported human dialogs\;&#32;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`&#32;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\.&#32;A failed pre\-switch handler is not automatically a veto\.
- A timeout cannot preempt synchronous JavaScript or magically cancel an arbitrary detached promise\.&#32;Code must honor cancellation and retire stale work\.

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

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

**Exercise\:**&#32;throw from&#32;`user_bash`&#32;to prohibit a shell command\.

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

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