qwen-code/integration-tests
易良 de706203c0
feat(core,cli,sdk): resume an interrupted turn without a synthetic "continue" message (#5030)
* 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>
2026-06-28 13:24:06 +00:00
..
baselines test(perf): add daemon baseline harness (#4175 Wave 1 PR 1) (#4205) 2026-05-17 00:41:26 +08:00
cli feat(core,cli,sdk): resume an interrupted turn without a synthetic "continue" message (#5030) 2026-06-28 13:24:06 +00:00
concurrent-runner refactor(tools): rename TodoWrite tool display name to TodoList (#5319) 2026-06-19 08:32:10 +08:00
fixtures feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
hook-integration feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827) 2026-04-16 10:10:33 +08:00
interactive feat(core): enable loop/cron tools by default (#4950) 2026-06-10 23:21:53 +08:00
sdk-typescript fix(test): count result messages instead of assistant messages in multi-model E2E test (#4341) 2026-05-20 10:42:51 +08:00
terminal-bench Terminal Bench Integration Test (#521) 2025-09-05 17:02:03 +08:00
terminal-capture test(integration): run no-AK smoke tests on PRs (#5607) 2026-06-22 19:41:32 +08:00
channel-plugin.test.ts docs(channels): add plugin developer guide and rename mock to plugin-example 2026-03-27 03:19:34 +00:00
fake-openai-server.test.ts test(integration): run no-AK smoke tests on PRs (#5607) 2026-06-22 19:41:32 +08:00
fake-openai-server.ts test(integration): run no-AK smoke tests on PRs (#5607) 2026-06-22 19:41:32 +08:00
globalSetup.ts feat(core): support QWEN_HOME env var to customize config directory (#2953) 2026-05-09 15:51:52 +08:00
test-helper.ts test(integration): harden flaky sleep-interception e2e against skipped tool calls (#4936) 2026-06-11 00:08:26 +08:00
test-mcp-server.ts # 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483) 2025-09-01 14:48:55 +08:00
tsconfig.json refactor(serve): 1 daemon = 1 workspace (#3803 §02) (#4113) 2026-05-15 12:44:36 +08:00
vitest.config.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
vitest.loadtest.config.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
vitest.terminal-bench.config.ts Fix E2E caused by Terminal Bench test (#529) 2025-09-08 10:51:14 +08:00