mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* feat(core): add continueInterruptedTurn to resume unfinished turns from history Detect how a session's last turn ended from persisted chat history alone (no in-memory request refs, so it works across process restarts) and continue it without injecting any synthetic user message: - interrupted_prompt (orphaned trailing user entry): re-submit the same content via SendMessageType.Retry — strip + re-push, same logical turn. - interrupted_turn (dangling functionCall): close each pair with a synthesized error functionResponse submitted as a ToolResult, the same continuation signal a real tool completion would have sent. - none: idempotent no-op (null stream). Core layer for the SDK/ACP "continue previous turn" capability discussed in #4679; protocol exposure (_qwen/session/continue) comes separately. * feat(cli,sdk): expose continue-interrupted-turn through ACP and SDK control protocol ACP surface: - New vendor ext method `qwen/session/continue` (acpAgent) backed by Session.continueTurn(): classifies the last turn from history and re-drives the model loop — Retry semantics for an orphaned trailing user entry, synthesized error tool results for dangling tool calls. No new user message enters the transcript; the prompt id reuses the interrupted turn's number. 409 when a turn is already in flight. - The model-call ↔ tool-call loop is extracted verbatim from #executePrompt into #runModelTurnLoop (+ #finishTurn epilogue) and shared by both paths. - initialize advertises the capability via agentCapabilities._meta (qwen.sessionContinue) per ACP extensibility guidance. SDK surface (stream-json control protocol): - New control request subtype `continue_last_turn` routed to SystemController and delegated to the nonInteractive Session, which schedules a continuation turn ahead of queued user messages. - runNonInteractive gains `continueInterrupted` mode: detection + Retry/ToolResult first-turn override; reminder injection is skipped so tool_result parts stay at the head of the user message. - Query.continueLastTurn() sends the request; reply reports { accepted, interruption } and resumed output flows as regular stream messages. Part of the #4679 continue-previous-turn work; core detection landed in the previous commit. * fix(core): replay the orphaned user entry verbatim, reminders included The Retry send path does not re-inject per-turn system reminders, so filtering them out of the captured entry made the resumed request differ from the original (the date reminder vanished). A continuation is the same turn, not a fresh one — capture the trailing user entry byte-for- byte so the resumed wire request is identical to the interrupted one. Verified end-to-end against a request-capturing mock server. * fix(cli): re-trigger work loop for pending continue turns; cover continueInterrupted - ensureProcessingStarted's finally-block re-trigger condition omitted pendingContinueTurns, so a continue request arriving as the work loop wound down could be stranded until the next user message. Add it to the guard alongside the other queues. - Add runNonInteractive({continueInterrupted}) unit tests: Retry path, ToolResult (interrupted_turn) path, and the clean-turn no-op. * fix(core,cli): restore orphaned entries when a continued send is skipped When resuming an interrupted turn, the orphaned trailing user entries are stripped from history before the content is re-sent. If that send is then skipped (e.g. session token limit exceeded), the stripped content was lost from history entirely. Have strip return the removed entries and restore them on any non-cancelled skip across the core retry, non-interactive, and ACP continue paths. On the ACP path the turn loop already re-adds functionResponse parts of an unsent message via #preserveUnsentMessageHistory, so a tool_result orphan would be written twice (once by preserve, once by restore). Let the caller own restoring the initial message and skip the loop's functionResponse-only preservation for that one message when it isn't a cancellation. * fix(cli,sdk): address continuation review feedback * fix(cli): address continuation review feedback * fix(cli): preserve continuation constraints and drain state * Merge latest main into feature branch * fix(core,cli): harden continuation recovery * fix(cli): close continuation review gaps * chore(cli): restore acp bridge path mapping * fix(cli): reject continuation during background turns * refactor(cli): simplify continuation handling * fix(cli): harden interrupted turn continuation * refactor(cli): share continuation helper logic * fix: harden interrupted-turn continuation * chore: remove unrelated continuation diff churn * refactor(cli): drop ACP session-continue surface from PR #5030 Issue #4679 only needs the SDK/stream-json continuation path. The SDK's Query.continueLastTurn() rides the stdio control protocol handled by the nonInteractive session, not ACP, and the qwen/session/continue vendor method had no SDK caller. Revert the ACP continuation surface (Session.continueTurn, #executeContinue, the qwen/session/continue routing and capability advertisement, and their tests) to the merge-base so the PR ships only the path the issue requires. * refactor(core): rename turnInterruption to kebab-case turn-interruption check-file/filename-naming-convention requires kebab-case for new files (AGENTS.md), so turnInterruption.ts failed CI lint. Rename the module and its test and update the index re-export. * fix(core,cli): delegate interrupted-prompt continuation to the Retry path The continuation path duplicated the orphan strip/restore that the Retry send in client.ts already performs, and its restore guard flipped on pre-push stream events, so an abort before the history push could permanently drop the orphaned user entry. Delegate to the Retry strip/restore, whose guard flips only after the content is committed. Removing the local strip drops the now-dead maxEntries parameter from stripOrphanedUserEntriesFromHistory, and detection now reads the full history (getHistory) so its re-submitted parts match the unbounded strip. Also fire onResultEmitted only after a successful emit, so a throwing emitResult cannot lose both the result and the error. * test(cli): cover insertAfterFunctionResponses ordering Add direct unit coverage for the splice helper used by the continuation reminder insertion: mixed parts, all-functionResponse input, empty additions, and empty parts. * feat(cli,acp,sdk): expose continue-last-turn to daemon + ACP and fix continuation review findings Surface the interrupted-turn continuation through two more entry points, both routing into a new Session.continueLastTurn() that reuses the core detectTurnInterruption classification and the existing send/tool loop: - daemon: new control method qwen/control/session/continue (status -> bridgeTypes -> bridge -> POST /session/:id/continue -> acpAgent handler), returning { accepted, interruption }. - ACP: a `qwen.daemon.continueLastTurn` prompt _meta flag, mirroring the existing daemon retry meta. interrupted_prompt re-submits the stripped orphaned run; interrupted_turn closes dangling tool calls with synthesized error functionResponses. System reminders are now inserted after any leading functionResponse parts so a tool-result continuation keeps tool_result blocks first (normal prompts unchanged). Also resolve the open continuation review findings: - core/client: history-length snapshot guard so a Retry send that fails before the first event never duplicates history (replaces the retrySendStarted flag that missed pre-event failures). - cli/nonInteractive/session: emit a terminal error result when an accepted continuation is abandoned by shutdown; emit a continue_turn_failed diagnostic when a continuation fails after a result was already flushed. - sdk/query: idempotent finishTransportRead with optional error, also invoked on the message-router error path, so a transport crash rejects pending control (continueLastTurn) and MCP requests instead of hanging. Adds regression tests across core/cli/sdk for each fix and the new surfaces. * test(sdk): cover finishTransportRead MCP-response rejection on teardown Addresses the review ask to validate that finishTransportRead rejects pending MCP responses when the transport read loop ends. White-box: injects a pending entry rather than standing up a full SDK MCP server handshake, since the rejection drain is map-internal regardless of how the entry was registered. * fix(acp): harden continueLastTurn — restore stripped orphan on send failure, fix ordering and error surfacing Review follow-ups on the ACP/daemon continuation path: - interrupted_prompt: preserve the full stripped orphan message back into history when a continuation send fails non-cancelled. Previously only functionResponse parts were kept, so a stripped text orphan was lost — the user's unanswered turn vanished from history. - worktree restore notice: insert after any leading functionResponse parts so a tool-result continuation keeps tool_result blocks first (Anthropic ordering), matching the system-reminder insertion. - continueLastTurn fire-and-forget: emit a client diagnostic on failure instead of log-only, so a failed continuation is visible rather than silently stalled. - log at warn when the in-prompt re-detection finds no interrupted turn. Adds a Session regression test asserting the orphan is preserved on a non-cancelled continuation send failure. * perf(acp,core): classify continuation from a bounded tail; O(1) retry length read Review follow-ups (all non-blocking): - continueLastTurn classifies from a shallow bounded tail (getHistoryTailShallow) instead of structuredClone-ing the entire history. The authoritative re-detection inside the fired prompt() still reads full history for the strip, matching the nonInteractive two-tier pattern. - the Retry history-length guard reads getHistoryLength() (O(1)) instead of getHistory().length, which structuredClones the whole history just to read .length (the truncateHistory path already warns about this). Adds a Session test for the pendingPrompt re-entrancy guard (continueLastTurn rejects while a prompt is in flight). * fix(acp-bridge): strip daemon continue meta key from external prompts An external POST /session/:id/prompt could set _meta['qwen.daemon.continueLastTurn'] and trigger a continuation through the prompt FIFO, bypassing continueLastTurn()'s admission guards. sendPrompt now strips it alongside the retry meta key; the legitimate path is the agent's internal Session.prompt() and does not go through sendPrompt. * fix(cli): resolve continuation review comments - Session.ts: reuse the shared insertAfterFunctionResponses helper for both the reminder and worktree-notice phases (removes the duplicated findIndex pattern) and guard against duplicate reminders on an interrupted_prompt continuation, mirroring nonInteractiveCli.ts. Adds an ordering test. - systemController.ts: point the missing-callback error at the wiring (onContinueLastTurn on ControlContext) instead of 'not available'. - nonInteractive/session.ts: tag the continue logs with sessionId. * fix(core): gate retry restore on a push counter, not history length The Retry strip/restore guard compared history length to a post-strip baseline to decide whether the re-submitted user content landed. Auto- compression inside GeminiChat.sendMessageStream runs before the push and shrinks history independently of it, so the length could fall below the baseline even after a successful push, restoring the stripped entries and duplicating the user's prompt (reachable on success once compaction removes >=2 entries, common near the token limit). GeminiChat now exposes a monotonic userContentPushCount (incremented when the user content is pushed, decremented only if that push is rolled back on a setup error). client.ts snapshots it after the strip and restores only when it did not advance — invariant under compression. * fix(sdk): log pending requests when the transport finalizes finishTransportRead rejected pending control/MCP requests with no anchor log, so a CLI crash mid-continuation left only scattered rejections. Emit one correlatable line with the pending counts and error when work was in flight. * fix(serve): track daemon continuation through the prompt-admission path POST /session/:id/continue ran the continuation as an untracked internal agent prompt (continueLastTurn fired Session.prompt() fire-and-forget via the sessionContinue control method). The bridge never set pendingPromptCount / promptActive / activePromptOriginatorClientId for it, so the daemon reported the session idle during a continuation, a racing POST /prompt was admitted with no FIFO wait and aborted the in-flight continuation, and the continuation's events went out unattributed. Now continueLastTurn is the accept/reject pre-check only; when it accepts, bridge.continueSession drives the actual turn through sendPrompt with a trusted continue flag (re-arming the continue meta key that sendPrompt strips from untrusted callers), so the continuation gets full admission tracking, FIFO serialization, originator attribution, and turn-complete broadcasting. The /continue route forwards X-Qwen-Client-Id and continueSession validates it up-front, matching POST /session/:id/prompt. The {accepted, interruption} response contract is preserved. Addresses the continuation-lifecycle review finding. * test(serve): cover continuation admission tracking end-to-end - bridge: assert a continuation reports the session BUSY (hasActivePrompt + pendingPromptCount) and that a racing prompt queues behind it via the FIFO instead of aborting it — the two behaviors the lifecycle fix targets. - serve: assert POST /session/:id/continue forwards X-Qwen-Client-Id to continueSession so the continuation turn is attributed like a prompt. * test(serve): add real-daemon smoke for POST /session/:id/continue Exercises the full route → bridge → agent continuation path against a spawned daemon: a fresh session has no interrupted turn, so /continue returns {accepted:false, interruption:'none'} (model-free, like the prompt-admission checks). Also fixes the now-stale DAEMON_CONTINUE_META_KEY comment to reflect that continueSession (not continueLastTurn) re-arms the meta via sendPrompt. * refactor(acp-bridge): drop dead AbortController in continueSession; harden reject test - continueSession passes undefined instead of a never-aborted AbortController to sendPrompt (continuations cancel via the cancelSession route, not this handle). - The 'rejected continueSession does not dispatch' test waits long enough for a regression fire-and-forget to reach the agent before asserting none ran, instead of a single microtask flush. * fix(cli): restore orphaned turn when a continuation send throws The interrupted_prompt continuation strips the orphaned user run from history before re-sending it. #preserveUnsentMessageHistory only restores on a graceful null-stream result, not when the send throws (e.g. a hard-rescue compression error before the user-content push) — so the orphan was permanently lost (transcript gap; --resume loads history missing the user's turn). Capture the stripped entries + a GeminiChat push-count snapshot, and restore them in the send-loop catch when the count did not advance (mirrors the core Retry restore in client.ts) — so a later tool-loop failure, where the orphan already landed, does not double-restore. Also emit conversation_finished on the no-op continuation early-return, which sits before the send-loop try/finally. * fix(serve): surface continuation admission failures + mirror prompt replay contract continueSession previously swallowed post-accept admission failures (queue-full, pre-aborted, session reaped) yet returned accepted:true, leaving SDK callers waiting for output that never arrives. Now it dispatches sendPrompt synchronously so an admission throw propagates (the caller gets an error, not a false accept); only post-admission turn failures are logged (now via teeServeDebugLine, so they reach the SSE diagnostic stream). It also mirrors POST /session/:id/prompt's replay/correlation contract: capture lastEventId + accept a route-generated promptId and return both on accept, so a client attaching the SSE stream can replay missed events and correlate turn_complete/turn_error. Adds tests for the combined retry+continue meta strip, nonexistent-session and async-failure error paths, and the new contract fields. * chore(core): log retry-restore decision for diagnostics restoreStrippedRetryEntries mutates history in a finally with no trace; a single debugLogger.info on the restore branch makes a future push-counter regression (silent duplicate/loss) diagnosable. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|---|---|---|
| .. | ||
| _daemon-benchmark-helpers.ts | ||
| _daemon-harness.ts | ||
| _daemon-perf-report.ts | ||
| acp-cron.test.ts | ||
| acp-integration.test.ts | ||
| cron-tools.test.ts | ||
| edit.test.ts | ||
| extensions-install.test.ts | ||
| file-system.test.ts | ||
| json-output.test.ts | ||
| json-schema.test.ts | ||
| list_directory.test.ts | ||
| mcp_server_cyclic_schema.test.ts | ||
| mock-acp-typecheck.test.ts | ||
| monitor.test.ts | ||
| notebook-edit.test.ts | ||
| qwen-config-dir.test.ts | ||
| qwen-daemon-loadtest.test.ts | ||
| qwen-daemon-startup-benchmark.test.ts | ||
| qwen-daemon-vs-cli-benchmark.test.ts | ||
| qwen-serve-baseline.test.ts | ||
| qwen-serve-routes.test.ts | ||
| qwen-serve-streaming.test.ts | ||
| read_many_files.test.ts | ||
| run_shell_command.test.ts | ||
| save_memory.test.ts | ||
| settings-migration.test.ts | ||
| simple-mcp-server.test.ts | ||
| sleep-interception.test.ts | ||
| stdin-context.test.ts | ||
| telemetry.test.ts | ||
| todo_write.test.ts | ||
| tool-search.test.ts | ||
| utf-bom-encoding.test.ts | ||
| write_file.test.ts | ||