diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index f78cd7c6..9f996059 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -16,14 +16,10 @@ export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats, } from "./providers/openai-codex-responses.js"; -export { - closeOpenAICodexWebSocketSessions, - getOpenAICodexWebSocketDebugStats, - resetOpenAICodexWebSocketDebugStats, -} from "./providers/openai-codex-responses.js"; export type { OpenAICompletionsOptions } from "./providers/openai-completions.js"; export type { OpenAIResponsesOptions } from "./providers/openai-responses.js"; export * from "./providers/register-builtins.js"; +export * from "./session-resources.js"; export * from "./stream.js"; export * from "./types.js"; export * from "./utils/diagnostics.js"; diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index a6a60a45..80eaf496 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -22,6 +22,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version import { getEnvApiKey } from "../env-api-keys.js"; import { clampThinkingLevel } from "../models.js"; +import { registerSessionResourceCleanup } from "../session-resources.js"; import type { Api, AssistantMessage, @@ -682,6 +683,8 @@ export function closeOpenAICodexWebSocketSessions(sessionId?: string): void { websocketSessionCache.clear(); } +registerSessionResourceCleanup(closeOpenAICodexWebSocketSessions); + function isWebSocketSseFallbackActive(sessionId: string | undefined): boolean { return sessionId ? websocketSseFallbackSessions.has(sessionId) : false; } diff --git a/packages/ai/src/session-resources.ts b/packages/ai/src/session-resources.ts new file mode 100644 index 00000000..d6300926 --- /dev/null +++ b/packages/ai/src/session-resources.ts @@ -0,0 +1,24 @@ +export type SessionResourceCleanup = (sessionId?: string) => void; + +const sessionResourceCleanups = new Set(); + +export function registerSessionResourceCleanup(cleanup: SessionResourceCleanup): () => void { + sessionResourceCleanups.add(cleanup); + return () => { + sessionResourceCleanups.delete(cleanup); + }; +} + +export function cleanupSessionResources(sessionId?: string): void { + const errors: unknown[] = []; + for (const cleanup of sessionResourceCleanups) { + try { + cleanup(sessionId); + } catch (error) { + errors.push(error); + } + } + if (errors.length > 0) { + throw new AggregateError(errors, "Failed to cleanup session resources"); + } +} diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 5e93e5f1..db2d6f0d 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -26,12 +26,12 @@ import type { import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai"; import { clampThinkingLevel, + cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "@mariozechner/pi-ai"; -import { closeOpenAICodexWebSocketSessions } from "@mariozechner/pi-ai/openai-codex-responses"; import { theme } from "../modes/interactive/theme/theme.js"; import { stripFrontmatter } from "../utils/frontmatter.js"; import { sleep } from "../utils/sleep.js"; @@ -753,7 +753,7 @@ export class AgentSession { ); this._disconnectFromAgent(); this._eventListeners = []; - closeOpenAICodexWebSocketSessions(this.sessionId); + cleanupSessionResources(this.sessionId); } // ========================================================================= diff --git a/packages/coding-agent/src/core/tools/read.ts b/packages/coding-agent/src/core/tools/read.ts index 08e4f825..432e5254 100644 --- a/packages/coding-agent/src/core/tools/read.ts +++ b/packages/coding-agent/src/core/tools/read.ts @@ -33,20 +33,6 @@ interface CompactReadClassification { label: string; } -interface ReadRenderState { - hideCall?: boolean; -} - -class ReadCallText extends Text { - constructor(private state: ReadRenderState) { - super("", 0, 0); - } - - override render(width: number): string[] { - return this.state.hideCall ? [] : super.render(width); - } -} - const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]); /** @@ -154,7 +140,7 @@ function getCompactReadClassification( return undefined; } -function formatCompactReadResult(classification: CompactReadClassification, theme: Theme): string { +function formatCompactReadCall(classification: CompactReadClassification, theme: Theme): string { if (classification.kind === "skill") { return ( theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) + @@ -180,11 +166,8 @@ function formatReadResult( cwd: string, isError: boolean, ): string { - if (!options.expanded && !isError) { - const classification = getCompactReadClassification(args, cwd); - if (classification) { - return formatCompactReadResult(classification, theme); - } + if (!options.expanded && !isError && getCompactReadClassification(args, cwd)) { + return ""; } const rawPath = str(args?.file_path ?? args?.path); @@ -216,7 +199,7 @@ function formatReadResult( export function createReadToolDefinition( cwd: string, options?: ReadToolOptions, -): ToolDefinition { +): ToolDefinition { const autoResizeImages = options?.autoResizeImages ?? true; const ops = options?.operations ?? defaultReadOperations; return { @@ -351,22 +334,16 @@ export function createReadToolDefinition( ); }, renderCall(args, theme, context) { - const text = - context.lastComponent instanceof ReadCallText ? context.lastComponent : new ReadCallText(context.state); - context.state.hideCall = false; - text.setText(formatReadCall(args, theme)); + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + const classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined; + text.setText(classification ? formatCompactReadCall(classification, theme) : formatReadCall(args, theme)); return text; }, renderResult(result, options, theme, context) { const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); - const hideCall = - !options.expanded && - !context.isError && - getCompactReadClassification(context.args, context.cwd) !== undefined; text.setText( formatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError), ); - context.state.hideCall = hideCall; return text; }, };