Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/coding-agent/src/core/agent-session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class AgentSessionRuntime {
}

private async teardownCurrent(): Promise<void> {
await this.session.waitForPendingEvents();
await emitSessionShutdownEvent(this.session.extensionRunner);
this.session.dispose();
}
Expand Down Expand Up @@ -285,6 +286,7 @@ export class AgentSessionRuntime {
}

async dispose(): Promise<void> {
await this.session.waitForPendingEvents();
await emitSessionShutdownEvent(this.session.extensionRunner);
this.session.dispose();
}
Expand Down
47 changes: 46 additions & 1 deletion packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import {
wrapRegisteredTools,
} from "./extensions/index.js";
import { buildDefaultCavememHooks } from "./hooks/cavemem-hooks.js";
import type { HooksConfig } from "./hooks/events.js";
import type { HookStdin, HooksConfig } from "./hooks/events.js";
import { createHooksExtension, HooksManager } from "./hooks/index.js";
import { composeStartupPrelude } from "./memory-bridge.js";
import { resolveMemoryProvider } from "./memory-factory.js";
Expand Down Expand Up @@ -1411,8 +1411,31 @@ export class AgentSession {
return undefined;
}

private async _dispatchStopHooks(): Promise<void> {
if (!this._hooksManager) return;

const depth = Number.parseInt(process.env.CAVE_SUBAGENT_DEPTH ?? "", 10);
const isSubagent = Number.isFinite(depth) && depth > 0;
const agentType = process.env.CAVE_SUBAGENT_NAME;
const agentId = process.env.CAVE_SUBAGENT_ID;

if (!isSubagent) {
await this._hooksManager.dispatch("Stop", undefined, { stop_hook_active: true });
return;
}

const subagentExtras: Partial<HookStdin> = { stop_hook_active: true };
if (agentType) subagentExtras.agent_type = agentType;
if (agentId) subagentExtras.agent_id = agentId;
await this._hooksManager.dispatch("SubagentStop", agentType ?? "*", subagentExtras);
}

/** Emit extension events based on agent events */
private async _emitExtensionEvent(event: AgentEvent): Promise<void> {
if (event.type === "agent_end") {
await this._dispatchStopHooks();
}

if (!this._extensionRunner) return;

if (event.type === "agent_start") {
Expand Down Expand Up @@ -1522,6 +1545,25 @@ export class AgentSession {
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
}

private async _drainAgentEventQueue(): Promise<void> {
let current = this._agentEventQueue;
while (true) {
try {
await current;
} catch {
// Event handlers already report through extension error paths; don't
// turn late lifecycle cleanup into a prompt failure.
}
if (this._agentEventQueue === current) return;
current = this._agentEventQueue;
}
}

/** Wait until queued session event handlers have settled. */
async waitForPendingEvents(): Promise<void> {
await this._drainAgentEventQueue();
}

/**
* Remove all listeners and disconnect from agent.
* Call this when completely done with the session.
Expand Down Expand Up @@ -1906,7 +1948,10 @@ export class AgentSession {
promptTimingMark("session.prompt:agent.prompt:begin");
await this.agent.prompt(messages);
promptTimingMark("session.prompt:agent.prompt:end");
await this._drainAgentEventQueue();
promptTimingMark("session.prompt:agent.events:end");
await this.waitForRetry();
await this._drainAgentEventQueue();
}

/**
Expand Down
9 changes: 4 additions & 5 deletions packages/coding-agent/src/core/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,10 @@ export function subscribeHooksToExtensionEvents(api: ExtensionEventBusLike, mana
reason: "prompt_input_exit",
} as any);
});
api.on("agent_end", async () => {
// "Stop" fires when the agent loop ends naturally. Claude Code
// uses Stop without a matcher.
await manager.dispatch("Stop", undefined, { stop_hook_active: true } as any);
});
// Stop/SubagentStop are dispatched directly by AgentSession when it
// observes agent_end. Keeping them out of this synthetic extension avoids
// losing hooks if the extension event bus is rebound or torn down near the
// end of a turn.
api.on("session_compact", async () => {
await manager.dispatch("PostCompact", "auto", { trigger: "auto" } as any);
});
Expand Down
Loading