qwen-code/integration-tests/cli
易良 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
..
_daemon-benchmark-helpers.ts test(cli): add daemon startup benchmark (#5825) 2026-06-25 06:56:13 +00:00
_daemon-harness.ts feat(mcp): reconcile MCP servers live on settings change (#5561) 2026-06-25 14:50:37 +00:00
_daemon-perf-report.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
acp-cron.test.ts feat(core): enable loop/cron tools by default (#4950) 2026-06-10 23:21:53 +08:00
acp-integration.test.ts revert(core): revert Protocol enum & model-identity decoupling (#5089) (#5745) 2026-06-23 16:32:38 +08:00
cron-tools.test.ts feat(core): enable loop/cron tools by default (#4950) 2026-06-10 23:21:53 +08:00
edit.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
extensions-install.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
file-system.test.ts fix(cli): keep v5 settings migration idempotent (#5676) 2026-06-23 01:28:48 +08:00
json-output.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
json-schema.test.ts fix(test): repair stale --json-schema integration assertion (#4075) 2026-05-12 12:00:57 +08:00
list_directory.test.ts Merge remote-tracking branch 'origin/main' into feat/in-session-cron-loops 2026-03-30 19:08:25 +08:00
mcp_server_cyclic_schema.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
mock-acp-typecheck.test.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
monitor.test.ts ci(e2e): stabilize MCP/CLI flows and cancel stale main runs (#4039) 2026-05-12 16:09:30 +08:00
notebook-edit.test.ts feat(core): add NotebookEdit tool for Jupyter notebooks 2026-05-21 00:06:15 +08:00
qwen-config-dir.test.ts revert(core): revert Protocol enum & model-identity decoupling (#5089) (#5745) 2026-06-23 16:32:38 +08:00
qwen-daemon-loadtest.test.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
qwen-daemon-startup-benchmark.test.ts test(cli): add daemon startup benchmark (#5825) 2026-06-25 06:56:13 +00:00
qwen-daemon-vs-cli-benchmark.test.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
qwen-serve-baseline.test.ts refactor(cli): Remove serve bridge re-export shims (#5955) 2026-06-28 13:20:44 +00:00
qwen-serve-routes.test.ts feat(core,cli,sdk): resume an interrupted turn without a synthetic "continue" message (#5030) 2026-06-28 13:24:06 +00:00
qwen-serve-streaming.test.ts ci: collapse PR checks into Ubuntu gate (#5767) 2026-06-23 22:08:43 +08:00
read_many_files.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
run_shell_command.test.ts ci(e2e): stabilize MCP/CLI flows and cancel stale main runs (#4039) 2026-05-12 16:09:30 +08:00
save_memory.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
settings-migration.test.ts revert(core): revert Protocol enum & model-identity decoupling (#5089) (#5745) 2026-06-23 16:32:38 +08:00
simple-mcp-server.test.ts fix(ci): restore release integration env controls (#5121) 2026-06-15 15:56:29 +08:00
sleep-interception.test.ts test(integration): harden flaky sleep-interception e2e against skipped tool calls (#4936) 2026-06-11 00:08:26 +08:00
stdin-context.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
telemetry.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
todo_write.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
tool-search.test.ts feat(core): enable loop/cron tools by default (#4950) 2026-06-10 23:21:53 +08:00
utf-bom-encoding.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00
write_file.test.ts refactor(tests): reorganize integration tests by execution mode 2026-03-29 05:49:17 +00:00