From b6d422ff6577e879ff24d6bd1eaec0d68721775a Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 7 Jun 2026 09:34:20 +0200 Subject: [PATCH] fix(ai): memoized PI_REQ_DEBUG response-log close to avoid truncated body on cancel --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/utils/request-debug.ts | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index d62e002fe..344d76b9a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -27,6 +27,7 @@ - Fixed the HTTP 400 raw-request dumper (`appendRawHttpRequestDumpFor400`) littering the real `~/.omp/logs/http-400-requests` directory during tests. Provider suites exercise the 400 error path with mocked `fetch` responses, which the dumper could not distinguish from genuine failures; it now skips persistence under the Bun test runner (`isBunTestRuntime()`). - Fixed Anthropic Opus requests unnecessarily forcing `tool_choice.disable_parallel_tool_use`, allowing Claude Opus to use the provider's default parallel tool-calling behavior again. - Fixed parallel `function_call` items losing arguments against llama.cpp's OpenAI Responses endpoint (`/v1/responses`), where every call but the last finalized with `{}` and the agent rejected them with `path: Invalid input: expected string, received undefined`. llama.cpp's `to_json_oaicompat_resp` emits `output_item.added` with only `item.call_id` (no `item.id`, no `output_index`) while the matching `function_call_arguments.delta` carries `item_id: "fc_"`. `processResponsesStream` now registers function-call and custom-tool-call items under `item.call_id` as a secondary lookup key (alongside `item.id`/`output_index`) so identifier-deviant hosts route deltas and done events to the right block. ([#2015](https://github.com/can1357/oh-my-pi/issues/2015)) +- Fixed `PI_REQ_DEBUG` response recording truncating the captured body when a streamed response was cancelled mid-flight. The response tee in `wrapResponse` could call `FileRequestDebugResponseLog.close()` from both the `cancel` callback and the resumed `pull` (which observes `done` once the source reader is cancelled); the second caller saw the handle already nulled and returned before the first caller's pending write flushed, so the `.res.log` lost the already-buffered chunk. `close()` now memoizes its flush-and-close promise so every caller awaits the same completion. ## [15.10.0] - 2026-06-06 diff --git a/packages/ai/src/utils/request-debug.ts b/packages/ai/src/utils/request-debug.ts index 193f51be6..daa6831c4 100644 --- a/packages/ai/src/utils/request-debug.ts +++ b/packages/ai/src/utils/request-debug.ts @@ -170,6 +170,7 @@ class FileRequestDebugSession implements RequestDebugSession { class FileRequestDebugResponseLog implements RequestDebugResponseLog { #handle: fs.FileHandle | undefined; #pending: Promise = Promise.resolve(); + #closed: Promise | undefined; constructor(handle: fs.FileHandle) { this.#handle = handle; @@ -184,15 +185,19 @@ class FileRequestDebugResponseLog implements RequestDebugResponseLog { }); } - async close(): Promise { + close(): Promise { + if (this.#closed) return this.#closed; const handle = this.#handle; - if (!handle) return; + if (!handle) return Promise.resolve(); this.#handle = undefined; - try { - await this.#pending; - } finally { - await handle.close(); - } + this.#closed = (async () => { + try { + await this.#pending; + } finally { + await handle.close(); + } + })(); + return this.#closed; } }