mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-08 00:05:37 +00:00
* feat(hooks): add MessageDisplay hook for mid-turn streaming Fires repeatedly as the assistant reply streams, before Stop (which only fires once at the end of the turn). Fire-and-forget, cumulative text payload, debounced (~200ms) except for the unconditional final firing. Fires from the single streaming loop in client.ts shared by the terminal UI and ACP paths. Fixes #6488 * fix(hooks): address MessageDisplay review feedback - Chain fire-and-forget MessageDisplay requests per message_id instead of firing them fully unbounded, so a slow hook command can't pile up concurrent processes. - Gate the final flush on non-empty displayed_text and !signal.aborted, matching the adjacent Stop hook's guard. - Document why the final flush intentionally re-sends the last debounced text (is_final itself is new information). - Simplify the debounce constant's JSDoc to drop the competitor comparison. - Add tests for the mid-stream debounced flush and the rejected-request warn path. * test(hooks): drain microtasks before asserting on chained MessageDisplay calls fireMessageDisplayHook now chains per-message_id through a promise (see previous commit), so the final flush's actual messageBus.request() call lands a few microtask ticks after the generator itself finishes — the mid-stream-flush test needs to let that chain settle before asserting. * fix(hooks): flush MessageDisplay is_final on every for-await exit path The three early `return turn` paths inside the streaming loop (always-on loop-detection safety, heuristic loop detection, and the stream Error event) exited before the final MessageDisplay flush, which only sat after the loop ended normally. Hook scripts relying on is_final: true to know when to flush never received it when a turn ended via loop detection or an API error. Extracts the flush into a shared closure and calls it from all four exits (the three early returns plus the normal fall-through), instead of only the one at the bottom of the loop. Adds regression tests for all three previously missed exits, plus the two guard-coverage tests requested in review (abort suppresses the flush, a tool-call-only turn with no Content events does not fire a vacuous empty-text event). Addresses the outstanding critical review comment and the follow-up test coverage suggestion on PR #6489. * fix(hooks): fire MessageDisplay on the ACP surface, coalesce delivery, drain is_final before turn end Addresses the three findings from the local verification report on #6489: - ACP/qwen serve (Finding 1): the delivery logic now lives in a shared MessageDisplayDispatcher (packages/core), and Session.ts wires it into all four raw-stream loops (main prompt, Stop-hook continuation, cron tick, background notification) — these surfaces consume GeminiChat's stream directly and never enter GeminiClient.sendMessageStream, so they need their own fire sites. The daemon no longer advertises an event it never emits. - Slow-hook backlog (Finding 2): the per-message promise chain is replaced by coalescing delivery — at most one in-flight request plus one pending payload per message; newer flushes overwrite the pending slot, which is lossless because displayed_text is cumulative, and is_final is sticky. A slow hook now sees fewer, newer payloads instead of an ever-growing queue of stale ones. - Headless is_final drop (Finding 3): finish() resolves only once every enqueued payload has actually been delivered, and every exit out of the streaming loops awaits it (early returns, normal fall-through, and the enclosing finally for uncaught exceptions), so a short-lived -p process can no longer exit with the final payload still queued. As a consequence, is_final delivery now strictly precedes the Stop hook rather than racing it. Also: the failure log line carries the message_id, finish() is idempotent, the review-requested tests are added (mid-stream and final firings share one message_id; isFinal as the sole flush reason), and hooks.md gains a delivery-semantics contract covering coalescing, the drain guarantee, no is_final on cancellation, provisional displayed_text, and multiple messages per tool-using turn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hooks): bound MessageDisplay drain wait, fix test gaps flagged in review finish() now gives up waiting on drain after 5s (MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS) instead of blocking turn teardown for up to the full 60s hook timeout, per the re-verification's S1 finding. Delivery keeps running in the background past the timeout; only the caller's wait is bounded. Also: add the config.ts bridge test for MessageDisplay field extraction (S5), and add the missing MessageDisplay/InstructionsLoaded entries to acpAgent.test.ts's HookEventName mock (S6). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hooks): dispatch MessageDisplay is_final alongside stale deliveries, share one drain budget Round-3 review findings on #6489: - finish() no longer queues the is_final payload behind an in-flight mid-stream delivery: the pending slot's supersession argument applies to the in-flight slot too, so the final payload is dispatched immediately, alongside the stale delivery if one is still running. is_final is handed to the hook the moment the message ends — before Stop — on every surface, and can no longer be dropped by a short-lived process exiting with it still queued (Finding 1). - The bounded drain wait is memoized: every finish() call (explicit, finally, or concurrent) shares one promise and one timer, so the teardown ceiling is MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS itself, not a multiple of it (Finding 2). - hooks.md delivery semantics rewritten to match the shipped behavior, including the headless orphaned-hook caveat and the unspecified completion order between an overlapped stale execution and the final one (Finding 3). - The dispatcher mirrors its warnings to console.warn itself (stderr on headless/ACP, ink patchConsole in the TUI) in addition to the injected debug-file sink, so hitting the drain timeout is visible by default (Finding 4). - A superseded mid-stream delivery that fails after the final was dispatched no longer warns; failures during streaming still do. - New tests: finish() twice while delivery is in flight (the exact client.ts sequence), concurrent finish() calls sharing one budget, is_final overtaking a held mid-stream delivery, and drain resolving on the final delivery alone. * refactor(core): consolidate MessageDisplay finish() calls, dedupe test spy setup client.ts: wrap the turn.run() streaming loop in try/finally so messageDisplay.finish() fires once instead of at each of the three early-return sites plus the post-loop path -- matching the pattern the four raw-stream loops in Session.ts already use for the same dispatcher. message-display-dispatcher.test.ts: centralize the console.warn spy setup/teardown in beforeEach/afterEach instead of five repeated per-test try/finally blocks. No behavior change: full client.test.ts (246/246) and the message-display-buffer/dispatcher suites (24/24) pass unchanged. * docs(hooks): clarify MessageDisplay cancellation timing (round-4 nit) * test(hooks): cover the 3 untested MessageDisplay dispatch sites, fix cancellation doc wording Adds MessageDisplay is_final coverage for the Stop-hook continuation loop, the in-session cron fire, and the background-notification loop, each with a normal-completion and an abort case. Adds three MessageDisplayDispatcher edge-case tests: a delivery settling just before the drain timeout, an abort arriving after a drain wait has already started, and addChunk called after abort but before finish(). Rewords the cancellation-timing doc bullet to state the actual criterion (abort signal state when finish() runs) rather than an approximation of it. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|---|---|---|
| .. | ||
| design | ||
| developers | ||
| e2e-tests | ||
| plans | ||
| superpowers | ||
| users | ||
| verification/abort-controller-refactor | ||
| _meta.ts | ||
| index.md | ||