Split HookContext and HookCommandContext to prevent deadlocks

HookContext (all events):
- isIdle() - read-only state check
- hasQueuedMessages() - read-only state check
- abort() - fire-and-forget, does not wait

HookCommandContext (slash commands only):
- waitForIdle() - waits for agent to finish
- newSession(options?) - create new session
- branch(entryId) - branch from entry
- navigateTree(targetId, options?) - navigate session tree

Session control methods moved from HookAPI (pi.*) to HookCommandContext (ctx.*)
because they can deadlock when called from event handlers that run inside
the agent loop (tool_call, tool_result, context events).
This commit is contained in:
Mario Zechner 2026-01-02 00:24:58 +01:00
parent ccdd7bd283
commit 0d9fddec1e
9 changed files with 170 additions and 203 deletions

View file

@ -30,7 +30,6 @@ import {
import type { CustomToolContext, CustomToolSessionEvent, LoadedCustomTool } from "./custom-tools/index.js";
import { exportSessionToHtml } from "./export-html/index.js";
import type {
HookContext,
HookRunner,
SessionBeforeBranchResult,
SessionBeforeCompactResult,
@ -545,24 +544,8 @@ export class AgentSession {
const command = this._hookRunner.getCommand(commandName);
if (!command) return false;
// Get UI context from hook runner (set by mode)
const uiContext = this._hookRunner.getUIContext();
if (!uiContext) return false;
// Build command context
const cwd = process.cwd();
const ctx: HookContext = {
ui: uiContext,
hasUI: this._hookRunner.getHasUI(),
cwd,
sessionManager: this.sessionManager,
modelRegistry: this._modelRegistry,
model: this.model,
isIdle: () => !this.isStreaming,
waitForIdle: () => this.agent.waitForIdle(),
abort: () => this.abort(),
hasQueuedMessages: () => this.queuedMessageCount > 0,
};
// Get command context from hook runner (includes session control methods)
const ctx = this._hookRunner.createCommandContext();
try {
await command.handler(args, ctx);