Merge remote-tracking branch 'origin/main' into feat/web-shell-app-host

This commit is contained in:
yiliang114 committed 2026-11-03 10:16:37 +08:00
commit 5683eed030
618 files changed
+47995 -15646

No files matched your search

+21
View File
@@ -11,6 +11,13 @@
# Usage: check-disk-floor.sh [DIR ...]
# With no directories, checks GITHUB_WORKSPACE and RUNNER_TEMP.
#
# The OS temp directory (TMPDIR, defaulting to /tmp) is ALWAYS checked in
# addition to the caller's directories (deduplicated): on 2026-10-31 the hz
# fleet's /tmp tmpfs hit 100% inodes while the workspace and RUNNER_TEMP sat
# on a data disk with millions free, so every gate was green and the jobs
# died on mkdtemp ENOSPC mid-suite. os.tmpdir() writes (node, surefire,
# tsx, git subprocess temp files) do not follow RUNNER_TEMP.
#
# Floors, checked per directory's filesystem (env-overridable):
# DISK_FLOOR_MIN_FREE_KB default 2097152 (2 GiB)
# DISK_FLOOR_MIN_FREE_INODES default 100000
@@ -41,6 +48,20 @@ if [ "$#" -eq 0 ]; then
set -- "${GITHUB_WORKSPACE:-.}" "${RUNNER_TEMP:-/tmp}"
fi
# Always include the OS temp directory: it is a distinct filesystem from the
# caller's directories on this fleet, and its exhaustion is exactly the
# failure class this gate exists to catch before the heavy step.
os_tmp="${TMPDIR:-/tmp}"
for existing in "$@"; do
if [ "$existing" = "$os_tmp" ]; then
os_tmp=''
break
fi
done
if [ -n "$os_tmp" ]; then
set -- "$@" "$os_tmp"
fi
status=0
for dir in "$@"; do
if [ ! -d "$dir" ]; then
+88
View File
@@ -124,4 +124,92 @@ describe('check-disk-floor', () => {
rmSync(temp, { recursive: true, force: true });
}
});
it('always checks the OS temp directory, deduplicated', () => {
// The 2026-10-31 hz incident: /tmp hit 100% inodes while the workspace
// and RUNNER_TEMP sat on a data disk, so caller directories alone were
// green while every mkdtemp died mid-suite. The gate must add the OS
// temp directory on its own — and not duplicate it when a caller
// already passed it.
const dir = mkdtempSync(join(tmpdir(), 'disk-floor-os-'));
const osTmp = mkdtempSync(join(tmpdir(), 'disk-floor-ostmp-'));
try {
// Explicit dirs: the OS temp dir is still checked (huge inode floor
// forces the breach on it deterministically).
const breached = runGate({
dirs: [dir],
env: {
TMPDIR: osTmp,
DISK_FLOOR_MIN_FREE_KB: '1',
DISK_FLOOR_MIN_FREE_INODES: '999999999999',
},
});
assert.equal(breached.status, 1);
assert.ok(breached.stdout.includes(`dir[${osTmp}]`));
assert.ok(breached.stdout.includes('free inodes'));
// Deduplication: passing the OS temp dir explicitly checks it once,
// and the sample set is exactly the caller's dirs (an appended-then-
// deduped path and a silently-dropped path differ only in set shape).
const counted = runGate({
dirs: [dir, osTmp],
env: {
TMPDIR: osTmp,
DISK_FLOOR_MIN_FREE_KB: '1',
DISK_FLOOR_MIN_FREE_INODES: '1',
},
});
assert.equal(counted.status, 0, counted.stderr);
const samples = counted.stdout
.split('\n')
.filter((line) => line.startsWith('DISKFLOOR '));
assert.equal(samples.length, 2);
const sampledDirs = samples
.map((line) => /dir\[([^\]]+)\]/.exec(line)?.[1])
.sort();
assert.deepEqual(sampledDirs, [dir, osTmp].sort());
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(osTmp, { recursive: true, force: true });
}
});
it('falls back to /tmp when TMPDIR is unset', () => {
// The incident fleet's os.tmpdir() is /tmp with TMPDIR unset; the
// script's `${TMPDIR:-/tmp}` default must keep gating that path. A
// regression to plain `${TMPDIR}` appends nothing and the incident
// class returns with the suite green. An empty-string TMPDIR is the
// bash-equivalent of unset for `:-` (but not for a bare expansion),
// which is exactly the distinction under test. RUNNER_TEMP is pinned
// to a distinct directory so a dir[/tmp] sample can only come from the
// OS-temp append, not the RUNNER_TEMP default (mutation-verified).
const ws = mkdtempSync(join(tmpdir(), 'disk-floor-ws-'));
const temp = mkdtempSync(join(tmpdir(), 'disk-floor-tmp-'));
try {
const result = runGate({
env: {
GITHUB_WORKSPACE: ws,
RUNNER_TEMP: temp,
TMPDIR: '',
DISK_FLOOR_MIN_FREE_KB: '1',
DISK_FLOOR_MIN_FREE_INODES: '1',
},
});
assert.equal(result.status, 0, result.stderr);
assert.ok(result.stdout.includes(`dir[${ws}]`));
assert.ok(result.stdout.includes(`dir[${temp}]`));
assert.ok(
result.stdout
.split('\n')
.some(
(line) =>
line.startsWith('DISKFLOOR ') && line.includes('dir[/tmp]'),
),
`expected the /tmp fallback sample, got: ${result.stdout}`,
);
} finally {
rmSync(ws, { recursive: true, force: true });
rmSync(temp, { recursive: true, force: true });
}
});
});
+13 -5
View File
@@ -18,8 +18,9 @@ replace it.
- Uses the existing Qwen sandbox daemon lock for labelled image pruning.
Build-cache pruning relies on Docker's in-use protection and runs even
when that lock is busy or missing.
- Changes the host `/tmp` retention policy from 30 days to 7 days through
`systemd-tmpfiles`.
- Changes the host `/tmp` retention policy from 30 days to 1 day through
`systemd-tmpfiles` (measured 2026-10-31: CI hosts generate ~120-260k
temp files/day and a 7d steady state exceeds the tmpfs inode ceiling).
Use this only on dedicated CI hosts. A sandbox-labelled or `qwen-code-*`-named
container older than 24 hours is considered leaked and may be removed even if
@@ -42,7 +43,7 @@ sudo .github/scripts/ecs-runner/install-qwen-docker-cleanup.sh
```
The installer copies the cleanup command and systemd units into the host,
installs the seven-day `/tmp` policy, reloads systemd, and enables the timer.
installs the one-day `/tmp` policy, reloads systemd, and enables the timer.
Run the same command again after pulling a newer version to update the host.
Merging changes does not update installed copies. Run the installer on every
host, including hosts that do not yet have the timer, and check each host's
@@ -56,14 +57,21 @@ systemctl is-enabled qwen-docker-cleanup.timer
systemctl is-active qwen-docker-cleanup.timer
systemctl list-timers qwen-docker-cleanup.timer
systemd-tmpfiles --cat-config | grep '^D /tmp '
journalctl -u qwen-docker-cleanup.service -n 5 --no-pager
```
The expected `/tmp` policy is:
```text
D /tmp 1777 root root 7d
D /tmp 1777 root root 1d
```
The retention policy is enforced by the daily `qwen-docker-cleanup.service`
itself (`ExecStartPost=systemd-tmpfiles --clean --prefix=/tmp`), not by the
distro's `systemd-tmpfiles-clean.timer` — on hardened images that timer can
be masked, and it reports `static` (unverifiable via `is-enabled`) on stock
Ubuntu. The journal line above confirms the ExecStartPost ran.
## Run manually
Drain the host first if it may contain a legitimate job or container running
@@ -110,7 +118,7 @@ the job log.
This is a temporary-directory convention, not filesystem isolation: commands
that explicitly write elsewhere bypass it. Existing arbitrary copies in `/tmp`
remain covered only by the seven-day policy. This service does not remove them
remain covered only by the one-day policy. This service does not remove them
by name because they may belong to an active job. A later review job on the
same runner registration retries removing stale `qwen-review-scratch.*`
directories from `RUNNER_TEMP`; residue on an idle registration still requires
+5 -2
View File
@@ -1,2 +1,5 @@
# Dedicated CI hosts generate large numbers of disposable test directories.
D /tmp 1777 root root 7d
# Dedicated CI hosts generate large numbers of disposable test directories
# (node-compile-cache, tsx, surefire, playwright — ~120-260k files/day).
# Measured 2026-10-31: a 7d steady state exceeds the 1,048,576-inode tmpfs
# ceiling; 1d keeps the steady state at 11-25% with the cleanup timer.
D /tmp 1777 root root 1d
@@ -12,3 +12,10 @@ Type=oneshot
# case instead of cutting the sweep off mid-prune.
TimeoutStartSec=1h
ExecStart=/usr/local/sbin/qwen-docker-cleanup
# Enforce the /etc/tmpfiles.d/tmp.conf (1d) retention here rather than
# relying on the distro's systemd-tmpfiles-clean.timer: on hardened images
# that timer can be masked, and it reports "static" on stock Ubuntu —
# unverifiable via is-enabled — while the fleet's measured 120-260k
# temp files/day regrows past the tmpfs inode ceiling without an
# enforcement agent (the 2026-10-31 incident class).
ExecStartPost=/usr/bin/systemd-tmpfiles --clean --prefix=/tmp
@@ -89,3 +89,32 @@ test('reports cache-prune failure instead of a successful service exit', () => {
assert.match(result.stderr, /Docker build cache cleanup failed/);
assert.match(result.calls, /^image prune --all --force/m);
});
test('installs a one-day /tmp retention policy', () => {
// Measured 2026-10-31: CI hosts generate ~120-260k temp files/day, and a
// 7d steady state exceeds the ~1M-inode tmpfs ceiling — that is the hz
// fleet incident. The installer's conf must pin 1d; a revert ships the
// incident again with every test green.
const conf = readFileSync(
new URL('./qwen-ci-tmp.conf', import.meta.url),
'utf8',
);
assert.match(conf, /^D \/tmp 1777 root root 1d$/m);
assert.doesNotMatch(conf, /^D \/tmp 1777 root root 7d$/m);
});
test('the daily service enforces the /tmp retention itself', () => {
// The distro's systemd-tmpfiles-clean.timer can be masked (hardened
// images) and reports "static" on stock Ubuntu — unverifiable via
// is-enabled — so the retention policy needs an enforcement agent the
// qwen stack owns. Dropping the ExecStartPost regrows the 2026-10-31
// incident class with every doc and test green.
const service = readFileSync(
new URL('./qwen-docker-cleanup.service', import.meta.url),
'utf8',
);
assert.match(
service,
/^ExecStartPost=\/usr\/bin\/systemd-tmpfiles --clean --prefix=\/tmp$/m,
);
});
+4 -4
View File
@@ -23,7 +23,7 @@
4193 codeql.yml
9389 comment-attachment-guard.yml
2090 desktop-packaging-check.yml
45089 desktop-release.yml
49858 desktop-release.yml
2038 docs-page-action.yml
10005 dsw-swe-verified-release.yml
40188 e2e.yml
@@ -41,13 +41,13 @@
5942 qwen-autofix-fork-signal.yml
469584 qwen-autofix.yml
7061 qwen-ci-flaky-rerun.yml
272192 qwen-code-pr-review.yml
276955 qwen-code-pr-review.yml
107075 qwen-fleet-shepherd.yml
22680 qwen-issue-followup-bot.yml
5760 qwen-pr-safety-precheck.yml
3243 qwen-review-runner-schedule.yml
27648 qwen-triage-finalize.yml
369831 qwen-triage.yml
375110 qwen-triage.yml
15726 release-chrome-extension.yml
26801 release-managed-agent-reference.yml
13464 release-managed-runtime.yml
@@ -60,7 +60,7 @@
1079 scorecard-monthly.yml
9751 sdk-java-contract-open-prs.yml
9555 sdk-java-flyway-open-prs.yml
144802 sdk-java.yml
150332 sdk-java.yml
3886 sdk-python.yml
5595 security-checks.yml
6777 serve-ab-publish.yml
+115 -7
View File
@@ -304,6 +304,7 @@ jobs:
# before the build. publish joins on both, so nothing is released
# unchecked.
runs-on: '${{ matrix.os }}'
container: ${{ matrix.container || null }} # yamllint disable-line rule:quoted-strings
timeout-minutes: 120
strategy:
fail-fast: false
@@ -311,23 +312,47 @@ jobs:
include:
- name: 'macOS arm64'
os: 'macos-latest'
container: ''
artifact_suffix: 'macos-arm64'
glibc_floor: ''
rpm: 'false'
epel: 'false'
legacy_arch: 'arm64'
rust_target: 'aarch64-apple-darwin'
tauri_config: ''
tauri_args: '--target aarch64-apple-darwin --bundles app,dmg'
- name: 'macOS x64'
os: 'macos-15'
container: ''
artifact_suffix: 'macos-x64'
glibc_floor: ''
rpm: 'false'
epel: 'false'
legacy_arch: 'x64'
rust_target: 'x86_64-apple-darwin'
tauri_config: ''
tauri_args: '--target x86_64-apple-darwin --bundles app,dmg'
- name: 'Windows x64'
os: 'windows-latest'
container: ''
artifact_suffix: 'windows-x64'
glibc_floor: ''
rpm: 'false'
epel: 'false'
legacy_arch: ''
rust_target: 'x86_64-pc-windows-msvc'
tauri_config: ''
tauri_args: '--target x86_64-pc-windows-msvc --bundles nsis'
- name: 'Linux x64'
os: 'ubuntu-22.04'
container: ''
artifact_suffix: 'linux-x64'
glibc_floor: '2.35'
rpm: 'false'
epel: 'false'
legacy_arch: ''
rust_target: 'x86_64-unknown-linux-gnu'
tauri_config: ''
tauri_args: '--target x86_64-unknown-linux-gnu --bundles appimage,deb'
# 22.04's ARM image, not 24.04's, so both Linux legs share a glibc
# floor (2.35): an arm64 artifact built on 24.04 would need 2.39 and
@@ -335,13 +360,61 @@ jobs:
# artifact still supports.
- name: 'Linux arm64'
os: 'ubuntu-22.04-arm'
container: ''
artifact_suffix: 'linux-arm64'
glibc_floor: '2.35'
rpm: 'false'
epel: 'false'
legacy_arch: ''
rust_target: 'aarch64-unknown-linux-gnu'
tauri_config: ''
tauri_args: '--target aarch64-unknown-linux-gnu --bundles appimage,deb'
# RPM is a per-root artifact: it links against the glibc and
# WebKitGTK of the root that builds it, so these two legs cannot
# reuse the 22.04 image the AppImage and .deb legs are pinned to.
# Their contract is recorded in `glibc_floor` and checked below
# instead of being inferred from `os:`.
- name: 'Linux x64 (rpm, Fedora 44)'
os: 'ubuntu-22.04'
container: 'docker.io/library/fedora@sha256:43b29f65a41eb9c35e1cd5323e3bdf3b655c2357a9f4f1ff2f9c2798e5045d80'
artifact_suffix: 'linux-x64-rpm-fedora44'
glibc_floor: '2.43'
rpm: 'true'
epel: 'false'
legacy_arch: ''
rust_target: 'x86_64-unknown-linux-gnu'
tauri_config: 'src-tauri/rpm-fedora44.conf.json'
tauri_args: '--target x86_64-unknown-linux-gnu --bundles rpm'
# RHEL 9 is deliberately absent: its webkit2gtk3 provides only the
# legacy 4.0 API, and Tauri needs libwebkit2gtk-4.1.so.0.
- name: 'Linux x64 (rpm, RHEL 10)'
os: 'ubuntu-22.04'
container: 'quay.io/centos/centos@sha256:99b8fe27dd4958b5dc2630ce35f5f6a7ff7bf9b2b1a72078102fde50d3277550'
artifact_suffix: 'linux-x64-rpm-rhel10'
glibc_floor: '2.39'
rpm: 'true'
epel: 'true'
legacy_arch: ''
rust_target: 'x86_64-unknown-linux-gnu'
tauri_config: 'src-tauri/rpm-rhel10.conf.json'
tauri_args: '--target x86_64-unknown-linux-gnu --bundles rpm'
env:
QWEN_CODE_COMMIT: '${{ needs.prepare.outputs.qwen_code_sha }}'
QWEN_DESKTOP_TARGET: '${{ matrix.rust_target }}'
steps:
- name: 'Bootstrap job container'
if: "matrix.container != ''"
shell: 'bash'
run: |
set -euo pipefail
packages=(
ca-certificates git tar gzip xz findutils which libstdc++ libatomic
)
if ! command -v curl >/dev/null 2>&1; then
packages+=(curl)
fi
dnf install -y "${packages[@]}"
- uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: '${{ needs.prepare.outputs.qwen_code_sha }}'
@@ -376,7 +449,7 @@ jobs:
id: 'node-cache'
with:
path: '${{ steps.node-cache-path.outputs.path }}'
key: 'desktop-node-v2-${{ matrix.rust_target }}-${{ env.NODE_VERSION }}-${{ inputs.dry_run }}'
key: 'desktop-node-v2-${{ matrix.artifact_suffix }}-${{ env.NODE_VERSION }}-${{ inputs.dry_run }}'
- uses: 'dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4' # stable
with:
@@ -385,13 +458,34 @@ jobs:
- uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2
with:
workspaces: 'packages/desktop/src-tauri -> target'
key: '${{ matrix.artifact_suffix }}'
- name: 'Install Linux dependencies'
if: "runner.os == 'Linux'"
if: "runner.os == 'Linux' && matrix.container == ''"
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libatk-bridge2.0-0 at-spi2-core dbus-x11 patchelf libfuse2 xdg-utils xvfb
- name: 'Install rpm build-root dependencies'
if: "matrix.container != ''"
shell: 'bash'
run: |
set -euo pipefail
if [ '${{ matrix.epel }}' = 'true' ]; then
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb
epel_rpm="$RUNNER_TEMP/epel-release-latest-10.noarch.rpm"
curl -fsSL --connect-timeout 15 --max-time 120 -o "$epel_rpm" 'https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm'
echo '8090214eb8b95617118478304aa5a8f796d4c19c625bc09e0c740cce0e682a34 '"$epel_rpm" | sha256sum -c -
dnf install -y "$epel_rpm"
fi
dnf install -y --setopt=install_weak_deps=False \
gcc gcc-c++ make cmake pkgconf-pkg-config python3 openssl-devel \
rpm cpio \
libappindicator-gtk3-devel librsvg2-devel \
gtk3 mesa-libgbm libdrm at-spi2-core dbus-x11 xdg-utils \
xorg-x11-server-Xvfb webkit2gtk4.1-devel
- name: 'Install Qwen Code dependencies'
shell: 'bash'
working-directory: '${{ runner.temp }}/qwen-code-source'
@@ -536,7 +630,7 @@ jobs:
if: "${{ steps.node-cache.outputs.cache-hit != 'true' }}"
with:
path: '${{ steps.node-cache-path.outputs.path }}'
key: 'desktop-node-v2-${{ matrix.rust_target }}-${{ env.NODE_VERSION }}-${{ inputs.dry_run }}'
key: 'desktop-node-v2-${{ matrix.artifact_suffix }}-${{ env.NODE_VERSION }}-${{ inputs.dry_run }}'
- name: 'Run desktop tests'
working-directory: 'packages/desktop'
@@ -619,7 +713,7 @@ jobs:
run: |
set -euo pipefail
if [ "$RUNNER_OS" = 'Linux' ]; then
QWEN_DESKTOP_PATCHELF="$(command -v patchelf)"
QWEN_DESKTOP_PATCHELF="$(command -v patchelf || true)"
PATCHELF="$GITHUB_WORKSPACE/packages/desktop/scripts/patchelf-immutable-runtime.sh"
export QWEN_DESKTOP_PATCHELF PATCHELF
fi
@@ -629,12 +723,25 @@ jobs:
elif [ "$RUNNER_OS" = 'Windows' ] && [ -n "$WINDOWS_CONFIG" ]; then
args+=(--config "$WINDOWS_CONFIG")
fi
# The rpm legs carry their runtime dependencies and glibc floor in a
# per-leg overlay: Tauri's rpm bundler writes `Requires:` only from
# `bundle.linux.rpm.depends`, and the two roots cannot share one floor.
if [ -n '${{ matrix.tauri_config }}' ]; then
args+=(--config '${{ matrix.tauri_config }}')
fi
npm run tauri -- build "${args[@]}"
- name: 'Verify Linux installer runtimes'
if: "runner.os == 'Linux'"
working-directory: 'packages/desktop'
run: 'npm run smoke:linux-installers -- src-tauri/target/${{ matrix.rust_target }}/release/bundle'
shell: 'bash'
run: |
set -euo pipefail
args=(src-tauri/target/${{ matrix.rust_target }}/release/bundle)
if [ '${{ matrix.rpm }}' = 'true' ]; then
args+=(--require rpm --expect-glibc '${{ matrix.glibc_floor }}')
fi
npm run smoke:linux-installers -- "${args[@]}"
- name: 'Verify macOS signature'
if: "runner.os == 'macOS' && (github.event_name == 'release' || inputs.dry_run == false)"
@@ -739,13 +846,14 @@ jobs:
esac
elif [ "$RUNNER_OS" = 'Linux' ]; then
case "$name" in
*.rpm) name="${name%.rpm}-${{ matrix.artifact_suffix }}.rpm" ;;
*.AppImage|*.AppImage.sig|*.deb|*.deb.sig) ;;
*) continue ;;
esac
fi
name="${name// /-}"
cp "$artifact" "$destination/$name"
done < <(find "$bundle_root" -mindepth 2 -maxdepth 2 -type f \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.exe' -o -name '*.zip' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0)
done < <(find "$bundle_root" -mindepth 2 -maxdepth 2 -type f \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*.exe' -o -name '*.zip' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0)
if [ -z "$(find "$destination" -type f -print -quit)" ]; then
echo '::error::No desktop artifacts were produced.'
exit 1
@@ -754,7 +862,7 @@ jobs:
- if: "${{ github.repository == 'QwenLM/qwen-code' }}"
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with:
name: 'desktop-${{ matrix.rust_target }}'
name: 'desktop-${{ matrix.artifact_suffix }}'
path: '${{ runner.temp }}/desktop-artifacts/*'
if-no-files-found: 'error'
retention-days: 14
+39 -1
View File
@@ -1054,6 +1054,13 @@ jobs:
REVIEW_MODEL_DEFAULT: '${{ vars.QWEN_PR_REVIEW_MODEL }}'
REVIEW_MODEL_CANARY: '${{ vars.QWEN_PR_REVIEW_MODEL_CANARY }}'
REVIEW_MODEL_CANARY_PERCENT: '${{ vars.QWEN_PR_REVIEW_MODEL_CANARY_PERCENT }}'
# The canary cohort may also carry its own endpoint: a second
# gateway with its own quota, or a provider the default gateway
# does not serve. The pair is all-or-nothing — a URL without its
# own key would send the default key to another provider, so the
# override below refuses that shape and keeps the default cohort.
REVIEW_BASE_URL_CANARY: '${{ vars.QWEN_PR_REVIEW_BASE_URL_CANARY }}'
REVIEW_API_KEY_CANARY: '${{ secrets.REVIEW_OPENAI_API_KEY_CANARY }}'
QWEN_PR_REVIEW_EFFORT: '${{ vars.QWEN_PR_REVIEW_EFFORT }}'
PR_NUMBER: '${{ steps.context.outputs.pr_number }}'
REVIEW_MODE: '${{ steps.context.outputs.review_mode }}'
@@ -1733,13 +1740,44 @@ jobs:
"${REVIEW_MODEL_DEFAULT:-}" "$REVIEW_MODEL_CANARY" \
"$CANARY_PERCENT_EFFECTIVE" "${PR_NUMBER:-}")"
export OPENAI_MODEL
# The canary cohort may carry its own endpoint as well as its own
# model: REVIEW_BASE_URL_CANARY/REVIEW_API_KEY_CANARY move the
# canary's traffic onto a second gateway so its quota is isolated
# from the default cohort's. The pair is all-or-nothing — a URL
# without its own key would send the default key to another
# provider — and an unparseable URL routes nowhere, so both shapes
# degrade to the default cohort, the same direction every canary
# misconfiguration takes.
REVIEW_BASE_URL_CANARY="${REVIEW_BASE_URL_CANARY:-}"
REVIEW_BASE_URL_CANARY="${REVIEW_BASE_URL_CANARY#"${REVIEW_BASE_URL_CANARY%%[![:space:]]*}"}"
REVIEW_BASE_URL_CANARY="${REVIEW_BASE_URL_CANARY%"${REVIEW_BASE_URL_CANARY##*[![:space:]]}"}"
REVIEW_COHORT=default
if [ -n "$REVIEW_MODEL_CANARY" ] &&
[ "${OPENAI_MODEL:-}" = "$REVIEW_MODEL_CANARY" ] &&
[ "$OPENAI_MODEL" != "${REVIEW_MODEL_DEFAULT:-}" ]; then
REVIEW_COHORT=canary
fi
if [ "$REVIEW_COHORT" = canary ] && [ -n "$REVIEW_BASE_URL_CANARY" ]; then
if [ -n "${REVIEW_API_KEY_CANARY:-}" ] &&
node -e 'try{if(!new URL(process.env.REVIEW_BASE_URL_CANARY).host)process.exit(1)}catch{process.exit(1)}' 2>/dev/null; then
OPENAI_BASE_URL="$REVIEW_BASE_URL_CANARY"
OPENAI_API_KEY="$REVIEW_API_KEY_CANARY"
export OPENAI_BASE_URL OPENAI_API_KEY
else
echo "::warning::canary endpoint unusable (REVIEW_OPENAI_API_KEY_CANARY missing or QWEN_PR_REVIEW_BASE_URL_CANARY unparseable); falling back to the default cohort."
OPENAI_MODEL="${REVIEW_MODEL_DEFAULT:-}"
REVIEW_COHORT=default
export OPENAI_MODEL
fi
fi
# The run's own record of which cohort served this review: the model
# is not carried anywhere else a later comparison could read it (the
# cost ledger has tokens, not a model), so a rollout that cannot be
# told apart afterwards is unmeasurable. Both fields are the resolved
# ones — the raw percent would certify a cohort the router clamped
# away.
echo "::notice title=Review model::${OPENAI_MODEL:-<CLI default>} (PR #${PR_NUMBER:-?}, canary ${CANARY_PERCENT_EFFECTIVE}%)"
echo "::notice title=Review model::${OPENAI_MODEL:-<CLI default>} (PR #${PR_NUMBER:-?}, canary ${CANARY_PERCENT_EFFECTIVE}%, cohort ${REVIEW_COHORT})"
MODEL_ARGS=()
if [ -n "${OPENAI_MODEL:-}" ]; then
+59 -2
View File
@@ -5975,6 +5975,8 @@ jobs:
FLAKE_LOG='verify-results/flake-gate.log'
ASSERT_LINE=''
ASSERT_LINE_ZH=''
GATES_LINE=''
GATES_LINE_ZH=''
if [ -n "$ASSERTIONS_FILE" ]; then
# Numbers only — coerce anything non-numeric to 0 so untrusted
# JSON cannot smuggle text into the comment body.
@@ -6001,6 +6003,39 @@ jobs:
read -r A_PASS A_FAIL A_TOTAL <<< "$ASSERT_META"
ASSERT_LINE="Scripted assertions: ${A_PASS} passed · ${A_FAIL} failed · ${A_TOTAL} total"
ASSERT_LINE_ZH="脚本断言:${A_PASS} 通过 · ${A_FAIL} 失败 · ${A_TOTAL} 总计"
# Existing test suites run as targeted gates are counted
# apart from the round's own assertions: folded in, a
# workspace's thousands of tests made "N scripted
# assertions" a measure of the repo, not of the round. Totals
# only, so no agent-chosen suite name reaches the body; a
# malformed list gets no line, and the checker refuses it.
# Shape rules mirror the checker's, and one shared fixture
# table pins the parity: a blank suite or a non-count skipped
# (false/null) is refused, not defaulted, and counts stop at
# 2^53-1, the last integer JSON literals parse to exactly.
# Blank means JSON whitespace only: jq's \S and JS's trim()
# disagree on U+0085 and U+FEFF.
GATES_META="$(
jq -r 'def count: type == "number" and . >= 0 and . == floor and . <= 9007199254740991;
if (.gates | type) == "array" and (.gates | length) > 0
and (.gates | all(type == "object" and (.suite | type) == "string" and (.suite | test("[^ \t\n\r]")) and (.pass | count) and (.fail | count) and ((has("skipped") | not) or (.skipped | count))))
then "\(.gates | length) \(.gates | map(.pass) | add | floor) \(.gates | map(.fail) | add | floor) \(.gates | map(.skipped // 0) | add | floor) \(.gates | map(select(.skipped != null)) | length)"
else empty end' "$ASSERTIONS_FILE" 2>/dev/null || true
)"
if [ -n "$GATES_META" ]; then
read -r G_SUITES G_PASS G_FAIL G_SKIP G_RECORDED <<< "$GATES_META"
# Absent skipped is "not recorded", not a measured zero,
# and a partial sum is not the run's total: the clause
# prints only when every entry carries the field.
G_SKIP_CLAUSE=''
G_SKIP_CLAUSE_ZH=''
if [ "$G_RECORDED" = "$G_SUITES" ]; then
G_SKIP_CLAUSE=" · ${G_SKIP} skipped"
G_SKIP_CLAUSE_ZH=" · ${G_SKIP} 跳过"
fi
GATES_LINE="Targeted gates (existing tests, not counted above): ${G_PASS} passed · ${G_FAIL} failed${G_SKIP_CLAUSE} across ${G_SUITES} suite(s)"
GATES_LINE_ZH="定向门禁(已有测试,不计入上面的断言):${G_PASS} 通过 · ${G_FAIL} 失败${G_SKIP_CLAUSE_ZH},共 ${G_SUITES} 个套件"
fi
else
echo "::warning::assertions.json missing or inconsistent; not treating it as evidence."
fi
@@ -6175,11 +6210,19 @@ jobs:
if [ -n "$ASSERT_LINE" ]; then
printf '%s\n\n' "$ASSERT_LINE"
fi
if [ -n "$GATES_LINE" ]; then
printf '%s\n\n' "$GATES_LINE"
# Advisory only: a gate can be red for a pre-existing main
# failure, so this never refuses or demotes the verdict.
if [ "$TRUST_AGENT_VERDICT" = true ] && [ "${G_FAIL:-0}" != '0' ]; then
printf 'Gate totals do not decide the verdict: a gate red the PR causes belongs in the assertion ledger above — read that count before trusting the verdict.\n\n'
fi
fi
if [ -n "$FLAKE_LINE" ]; then
printf '%s\n\n' "$FLAKE_LINE"
fi
if [ "${AGENT_VERDICT:-}" = 'merge-ready' ] && [ "$TRUST_AGENT_VERDICT" != true ] && [ "${A_FAIL:-0}" != '0' ]; then
printf 'The agent reported `merge-ready`, but `assertions.json` recorded %s failures, so that claim was not trusted — see the report for whether these are expected A/B control cells.\n\n' "$A_FAIL"
printf 'The agent reported `merge-ready`, but `assertions.json` recorded %s failures, so that claim was not trusted: an expected A/B control red counts as a pass, so a recorded failure contradicts `merge-ready`.\n\n' "$A_FAIL"
fi
if [ -n "$CHECK_NOTE" ]; then
printf 'The agent reported `%s`, but %s.\n\n' "$AGENT_VERDICT" "$CHECK_NOTE"
@@ -6203,11 +6246,17 @@ jobs:
if [ -n "$ASSERT_LINE_ZH" ]; then
printf '%s\n\n' "$ASSERT_LINE_ZH"
fi
if [ -n "$GATES_LINE_ZH" ]; then
printf '%s\n\n' "$GATES_LINE_ZH"
if [ "$TRUST_AGENT_VERDICT" = true ] && [ "${G_FAIL:-0}" != '0' ]; then
printf '门禁合计不影响判定:PR 导致的门禁红灯应记入上面的断言账本——采信判定前请先看那个数。\n\n'
fi
fi
if [ -n "$FLAKE_LINE_ZH" ]; then
printf '%s\n\n' "$FLAKE_LINE_ZH"
fi
if [ "${AGENT_VERDICT:-}" = 'merge-ready' ] && [ "$TRUST_AGENT_VERDICT" != true ] && [ "${A_FAIL:-0}" != '0' ]; then
printf 'agent 报告了 `merge-ready`,但 `assertions.json` 记录了 %s 个失败,因此该判定未被采信——请参阅报告确认这些是否为预期的 A/B 对照单元。\n\n' "$A_FAIL"
printf 'agent 报告了 `merge-ready`,但 `assertions.json` 记录了 %s 个失败,因此该判定未被采信:预期中的 A/B 对照红灯应计为通过,所以记录在案的失败与 `merge-ready` 相矛盾。\n\n' "$A_FAIL"
fi
if [ -n "$CHECK_NOTE_ZH" ]; then
printf 'agent 报告了 `%s`,但%s。\n\n' "$AGENT_VERDICT" "$CHECK_NOTE_ZH"
@@ -6292,6 +6341,14 @@ jobs:
if [ "$OWN_SUBSTANTIVE" = true ] && [ "$BODY_SUBSTANTIVE" != true ]; then
OWN_ID=''
fi
# A round can queue for hours: a notice with no status of its own
# to finalize is noise on a PR that closed meanwhile. A failed
# lookup posts.
if [ -z "$OWN_ID" ] && [ "$BODY_SUBSTANTIVE" != true ] &&
[ "$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.state' 2>/dev/null || true)" = closed ]; then
echo "PR #${PR_NUMBER} closed before this round published; notice not posted (verdict=${VERDICT})." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# A failed PATCH (comment deleted, or ownership changed under us)
# must still leave a terminal report on the PR, never silence.
if [ -n "$OWN_ID" ]; then
@@ -152,6 +152,32 @@ jobs:
echo 'The complete SDK and server verification gates remain enabled.'
} >> "${GITHUB_STEP_SUMMARY}"
- name: 'Repair the 0.26.5 release inputs'
if: "${{ env.RELEASE_TAG == 'v0.26.5' }}"
run: |-
set -euo pipefail
if [[ "${SOURCE_SHA}" != 'db0e4e24a0d89b211b2a15f4af2be22adac71dfc' ]]; then
echo 'The 0.26.5 release repair requires the original release commit.' >&2
exit 1
fi
javadoc_patch='.qwen/managed-release-tools/scripts/patches/managed-runtime-v0.26.5-javadoc.patch'
verification_patch='.qwen/managed-release-tools/scripts/patches/managed-runtime-v0.26.5-verification.patch'
printf '%s\n' \
"f0f569810b4966857fc173848386cda2cfb77acd9c3bd6239836c061ce97aadf ${javadoc_patch}" \
"14deffe00637abf59d475acdf870a5725d040f75c787308f8d822ab9b47cfa2c ${verification_patch}" | sha256sum --check -
git diff --exit-code HEAD
git apply --check "${javadoc_patch}" "${verification_patch}"
git apply "${javadoc_patch}" "${verification_patch}"
{
echo '### Managed runtime 0.26.5 release input repair'
echo "Product source: ${SOURCE_SHA}; original tag unchanged."
echo 'Javadoc-only repair from #16099: escape the numeric range and resolve its documentation link.'
echo 'Restore profile-owned integration tests without changing tests or their selectors.'
echo "Javadoc patch blob: $(git hash-object "${javadoc_patch}")"
echo "Verification patch blob: $(git hash-object "${verification_patch}")"
echo 'The complete CLI, SDK and server verification gates remain enabled.'
} >> "${GITHUB_STEP_SUMMARY}"
- name: 'Build the matching CLI prerequisites'
env:
QWEN_SKIP_PREPARE: '1'
+76 -19
View File
@@ -299,6 +299,13 @@ jobs:
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-external" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-host" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-http" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-local" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-kubernetes" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-ssh" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-static" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
# The suite resolves from the job-local copy above, so it does not
@@ -431,7 +438,7 @@ jobs:
mkdir -p "${HOME}/.m2/repository" 9>&-
cd "${RUNNER_TEMP}/m2-repo"
write_back=0
timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-parent -o -path ./com/alibaba/qwen-managed-agent-contracts -o -path ./com/alibaba/qwen-managed-agent-control-plane -o -path ./com/alibaba/qwen-managed-agent-runtime-broker -o -path ./com/alibaba/qwen-managed-agent-persistence -o -path ./com/alibaba/qwen-managed-agent-adapters -o -path ./com/alibaba/qwen-managed-agent-server -o -path ./com/alibaba/qwen-managed-agent-verification -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$?
timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-parent -o -path ./com/alibaba/qwen-managed-agent-contracts -o -path ./com/alibaba/qwen-managed-agent-control-plane -o -path ./com/alibaba/qwen-managed-agent-runtime-broker -o -path ./com/alibaba/qwen-managed-agent-persistence -o -path ./com/alibaba/qwen-managed-agent-adapters -o -path ./com/alibaba/qwen-managed-agent-runtime-external -o -path ./com/alibaba/qwen-managed-agent-runtime-host -o -path ./com/alibaba/qwen-managed-agent-runtime-http -o -path ./com/alibaba/qwen-managed-agent-runtime-local -o -path ./com/alibaba/qwen-managed-agent-runtime-kubernetes -o -path ./com/alibaba/qwen-managed-agent-runtime-ssh -o -path ./com/alibaba/qwen-managed-agent-runtime-static -o -path ./com/alibaba/qwen-managed-agent-server -o -path ./com/alibaba/qwen-managed-agent-verification -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$?
if [ "${write_back}" -eq 124 ]; then
echo "::warning::Maven cache write-back stopped after 300 s; the gate result above is unaffected"
elif [ "${write_back}" -ne 0 ]; then
@@ -939,6 +946,13 @@ jobs:
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-external" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-host" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-http" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-local" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-kubernetes" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-ssh" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-static" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
# Both Maven runs resolve from the job-local copy above, so they do
@@ -1045,7 +1059,7 @@ jobs:
mkdir -p "${HOME}/.m2/repository" 9>&-
cd "${RUNNER_TEMP}/m2-repo"
write_back=0
timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-parent -o -path ./com/alibaba/qwen-managed-agent-contracts -o -path ./com/alibaba/qwen-managed-agent-control-plane -o -path ./com/alibaba/qwen-managed-agent-runtime-broker -o -path ./com/alibaba/qwen-managed-agent-persistence -o -path ./com/alibaba/qwen-managed-agent-adapters -o -path ./com/alibaba/qwen-managed-agent-server -o -path ./com/alibaba/qwen-managed-agent-verification -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$?
timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-parent -o -path ./com/alibaba/qwen-managed-agent-contracts -o -path ./com/alibaba/qwen-managed-agent-control-plane -o -path ./com/alibaba/qwen-managed-agent-runtime-broker -o -path ./com/alibaba/qwen-managed-agent-persistence -o -path ./com/alibaba/qwen-managed-agent-adapters -o -path ./com/alibaba/qwen-managed-agent-runtime-external -o -path ./com/alibaba/qwen-managed-agent-runtime-host -o -path ./com/alibaba/qwen-managed-agent-runtime-http -o -path ./com/alibaba/qwen-managed-agent-runtime-local -o -path ./com/alibaba/qwen-managed-agent-runtime-kubernetes -o -path ./com/alibaba/qwen-managed-agent-runtime-ssh -o -path ./com/alibaba/qwen-managed-agent-runtime-static -o -path ./com/alibaba/qwen-managed-agent-server -o -path ./com/alibaba/qwen-managed-agent-verification -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$?
if [ "${write_back}" -eq 124 ]; then
echo "::warning::Maven cache write-back stopped after 300 s; the gate result above is unaffected"
elif [ "${write_back}" -ne 0 ]; then
@@ -1097,7 +1111,7 @@ jobs:
name: 'Hosted process fault gates / MySQL 8.4 / Java 21'
if: "${{ inputs.tier != 'fast' }}"
runs-on: 'ubuntu-latest'
# The step ceilings today (162 + 20 + 50 + 20 = 252 — the FG8 letters
# The step ceilings today (204 + 20 + 50 + 20 = 294 — the FG8 letters
# grew the shared failsafe fork to 3600 s, FG9 slice 3 to 3990 s, the
# FG10b–f workflow relay letters to 4484 s, the FG12a MCP reply-loss
# letter to 4499 s, the FG12c restart matrix to 4548 s, the FG13c Hook
@@ -1168,13 +1182,27 @@ jobs:
# Verify at 198 minutes, wrapping fork + 600 s plus margin (11864 s of
# fork plus wrap, 16 s left). The pool fixture provisionally reserves
# 180 s (its 120 s method ceiling plus 60 s for process/context cleanup),
# pending CI measurement: fork 11444 s, Verify 201 minutes, and 12044 s
# of fork plus wrap with the same 16 s left;
# pending CI measurement: fork 11444 s and Verify 201 minutes; the FG9e
# cursor-expired-resync case adds 60 s (21–23 s measured), so the fork
# moves to 11504 s and Verify to 202 minutes, wrapping fork + 600 s plus
# margin (12104 s of fork plus wrap, 16 s left); the FG12d two-server
# release cell adds 32 s (16.2 s measured), so the fork moves to 11536 s
# and Verify to 203 minutes, wrapping fork + 600 s plus margin (12136 s
# of fork plus wrap, 44 s left); the FG12d detach-unknown cell adds 55 s
# (27.4 s measured), so the fork moves to 11591 s and Verify to 204
# minutes, wrapping fork + 600 s plus margin (12191 s of fork plus wrap,
# 49 s left);
# the Broker fault gates use 20,
# and admission plus O4 keep per-fork ceilings) plus ~13 minutes for the
# uncapped setup steps (Checkout.. cgroup delegation, the report
# checks): 291 + 13. The failover arms run in hosted-failover-e2e.
timeout-minutes: 304
# and admission plus O4 keep per-fork ceilings) plus a 33-minute
# allowance for the uncapped setup steps (Checkout.. cgroup delegation,
# the report checks), which took under 10 minutes on every nightly from
# 10-14 to 10-30: 294 + 33. A Verify that timed out used to skip
# admission, leaving that path 50 minutes spare; admission now runs
# after it, so that path too can fill all four capped ceilings. The
# allowance sits above the ~13-minute floor so that a slow setup (the
# CLI install makes up to three attempts) still leaves the O4 gates
# time to run. The failover arms run in hosted-failover-e2e.
timeout-minutes: 327
services:
mysql:
image: 'mysql:8.4.6'
@@ -1274,10 +1302,10 @@ jobs:
# fork leaves no per-test failure lines, so the main-CI failure
# analyzer can only file an undiagnosable per-commit issue (#13503).
# hosted-process-ci.test.js pins this ordering; the fork budget is
# 11444 s, so the step wraps to fork + 600 s plus margin (201 min,
# 16 s left before the step must move), including the provisional
# 11591 s, so the step wraps to fork + 600 s plus margin (204 min,
# 49 s left before the step must move), including the provisional
# 180 s pool-fixture reservation pending CI measurement.
timeout-minutes: 201
timeout-minutes: 204
env:
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
MYSQL_PORT: "${{ job.services.mysql.ports['3306'] }}"
@@ -1329,24 +1357,39 @@ jobs:
retention-days: 7
- name: 'Verify Hosted admission gates'
id: 'hosted_admission'
# Its clean would otherwise discard the hosted lane's failsafe
# reports and the fg7-cases evidence the hosts upload above, so it
# runs after the fixture. The hosted-harness-gates profile carries a
# 2400 s failsafe fork budget for both admission witness classes
# (@Timeout(1800) each); the same killed-after-timeout pattern as the
# hosted-harness-mysql verify applies.
# Like the fault gates it rebuilds the module and needs only what the
# bundle step installs and builds, the installed SDK and the MySQL
# service, so a red Hosted verify must not skip it: without this it
# was skipped on all 13 nights from 10-14 to 10-30 that the verify
# failed, three of them after the fault gates got this condition.
# It boots on a fresh database, as O4 does: schema validation refuses
# any table outside the managed set, and a Hosted IT killed before
# its cleanup can leave its fg*_fire table behind.
if: "${{ !cancelled() && steps.managed_dependencies.outcome == 'success' }}"
timeout-minutes: 50
env:
MAVEN_ARGS: '--settings ${{ runner.temp }}/setup-java-m2/settings.xml --toolchains ${{ runner.temp }}/setup-java-m2/toolchains.xml'
MYSQL_PORT: "${{ job.services.mysql.ports['3306'] }}"
run: >-
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml
-Phosted-harness-gates
-Dnode.executable="$(command -v node)"
-Dqwen.cli.entry="${GITHUB_WORKSPACE}/dist/cli.js"
-Dmysql.url="jdbc:mysql://127.0.0.1:${MYSQL_PORT}/hosted_harness_test?allowPublicKeyRetrieval=true&useSSL=false"
-Dmysql.user=root -Dmysql.password=hosted-fixture clean verify checkstyle:check
MYSQL_PWD: 'hosted-fixture'
run: |-
mysql --no-defaults --protocol=TCP --host=127.0.0.1 --port="${MYSQL_PORT}" --user=root -e 'CREATE DATABASE qwen_admission_ci'
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml \
-Phosted-harness-gates \
-Dnode.executable="$(command -v node)" \
-Dqwen.cli.entry="${GITHUB_WORKSPACE}/dist/cli.js" \
-Dmysql.url="jdbc:mysql://127.0.0.1:${MYSQL_PORT}/qwen_admission_ci?allowPublicKeyRetrieval=true&useSSL=false" \
-Dmysql.user=root -Dmysql.password=hosted-fixture clean verify checkstyle:check
- name: 'Check that every Hosted admission gate class ran'
# It reads only the admission step's reports, so it follows that
# step rather than the job status a red Hosted verify has already set.
if: "${{ !cancelled() && steps.hosted_admission.outcome == 'success' }}"
run: 'node scripts/check-failsafe-reports.js hosted-gates packages/managed-agent-server'
- name: 'Verify O4 filesystem process and capacity gates'
@@ -1808,6 +1851,13 @@ jobs:
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-external" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-host" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-http" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-local" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-kubernetes" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-ssh" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-static" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install
@@ -2097,6 +2147,13 @@ jobs:
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-external" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-host" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-http" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-local" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-kubernetes" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-ssh" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-static" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install
@@ -116,6 +116,14 @@ jobs:
|| { echo "::error::Missing Desktop installer matching *.AppImage"; exit 1; }
find . -maxdepth 1 -name '*.deb' -print -quit | grep -q . \
|| { echo "::error::Missing Desktop installer matching *.deb"; exit 1; }
# A fresh build must publish both rpm roots. Re-mirroring an older
# release stays tolerant because those releases predate rpm.
if [ "$SOURCE" != 'release' ]; then
for suffix in linux-x64-rpm-fedora44 linux-x64-rpm-rhel10; do
find . -maxdepth 1 -name "*-${suffix}.rpm" -print -quit | grep -q . \
|| { echo "::error::Missing Desktop rpm for ${suffix}"; exit 1; }
done
fi
rm -f desktop-latest.json SHA256SUMS.txt
# Mirroring an existing GitHub release has to be able to reproduce the
# feed that release actually shipped, so a platform added later (arm64
+7
View File
@@ -113,6 +113,13 @@ jobs:
qwen-managed-agent-runtime-broker
qwen-managed-agent-persistence
qwen-managed-agent-adapters
qwen-managed-agent-runtime-external
qwen-managed-agent-runtime-host
qwen-managed-agent-runtime-http
qwen-managed-agent-runtime-local
qwen-managed-agent-runtime-kubernetes
qwen-managed-agent-runtime-ssh
qwen-managed-agent-runtime-static
qwen-managed-agent-server
qwen-managed-agent-verification
acp-sdk
@@ -0,0 +1,78 @@
# Mod prompt.submit in headless runs
Design: [docs/design/2026-10-30-mod-prompt-submit.md](../../docs/design/2026-10-30-mod-prompt-submit.md).
## Setup
- Build the CLI and bundle it: `npm run build && npm run bundle`.
- Use an isolated `HOME` whose `.qwen/settings.json` sets `experimental.mods` to `true`, selects `openai` auth and points `OPENAI_BASE_URL` at a loopback OpenAI-compatible fixture that answers `OK` and records every request.
- Install a guard Mod with `qwen extensions install ./guard-mod --consent`. Its `hooks/register.js`:
```javascript
export function register(on) {
on('prompt.submit', async ($, e, next) => {
$.ui.log('submit:' + JSON.stringify(e));
if (/AKIA[0-9A-Z]{16}/.test(e.text)) return { drop: 'the prompt holds an AWS access key' };
if (e.text === 'boom') throw new Error('guard broke');
if (e.text === 'mine') return { text: 'answered without next' };
const entered = await next({
...e,
text: e.text.replace(/\bprod\b/g, 'production'),
context: [
...(e.context ?? []),
e.text.startsWith('note') ? 'Mod note: ' + e.text : 'Mod context: <be brief>',
],
});
$.ui.log('entered:' + JSON.stringify(entered));
return entered;
}).catch(($, e, next) => (next.called ? next(e) : { drop: 'the guard failed' }));
on('turn.start', ($, e, next) => {
$.ui.log('turn.start:' + e.text);
return next(e);
});
}
```
## Test groups
### Rewrite and context
- `qwen -p 'deploy prod now' --output-format json`: exit 0. The model reads `deploy production now` followed by the hidden block `Mod context: &lt;be brief&gt;`. `turn.start` hears `deploy production now`. The transcript user record has `displayText: "deploy production now"` and the block as `hookContext`.
### Drop
- `qwen -p 'key AKIAABCDEFGHIJKLMNOP' --output-format json`: exit 1, no model request, error result `Prompt dropped by a hook: the prompt holds an AWS access key`.
- The same prompt in text output: exit 1, the reason on stderr.
- `-p boom`, where the guard throws and its `.catch` drops: exit 1, no model request, `Prompt dropped by a hook: the guard failed`.
- `-p mine`, where the guard answers without `next`: exit 1, no model request, `Prompt not submitted: a hook answered without passing the prompt on`.
### stream-json
- A message holding a key, then `ship prod`: the first message ends with the error result, the session goes on, and the second reaches the model as `ship production`.
- A message `[text 'ship prod', image block]`: the model reads `ship production` with the serialized image block after it, and the transcript `displayText` keeps the block after the rewritten text, the same shape it has without a rewrite.
### Context sent once per context
Add a settings `UserPromptSubmit` command hook that prints `settings-note`, then send `note one`, `note two`, `note two` in one stream-json session:
- `note one`: block `Mod note: note one\n\nsettings-note`.
- `note two`: block `Mod note: note two` only; the unchanged `settings-note` is not sent again.
- `note two` again: no block; both contexts are already in the conversation.
## Results (2026-11-02, branch head after merging main)
Built CLI, real fixture server, all groups above pass:
| Run | Result |
| --- | --- |
| rewrite (json) | exit 0, model reads `deploy production now` + context block, transcript `displayText` rewritten |
| drop (json / text) | exit 1, 0 model requests, drop reason as error result / on stderr |
| guard throws, `.catch` drops | exit 1, 0 model requests, `the guard failed` |
| answer without `next` | exit 1, 0 model requests, `Prompt not submitted: ...` |
| stream-json drop then rewrite | first message error, second sent as `ship production` |
| stream-json rewrite with an image block | transcript `displayText` = `ship production\n{"type":"image",...}` |
| context sent once per context | blocks `["Mod note: note one\n\nsettings-note"]`, `["Mod note: note two"]`, `[]` |
## Baseline
`main` at `619a601b39`, bundled locally, skips every `prompt.submit` hook at startup (`Hooks for unsupported events will not run: prompt.submit`): the key prompts reach the model with exit 0, and `turn.start` hears the original text. The global `qwen` 0.25.1 on this machine is a standalone binary, which the fixture driver (it starts the CLI with `node`) cannot run, so the baseline comes from the `main` bundle.
+57 -18
View File
@@ -1,7 +1,7 @@
---
name: verify-pr
description: This skill should be used to run a sandboxed deep verification of a qwen-code PR — "/verify-pr <n>", "/verify-pr <n> --publish" (local round that also posts the report comment), "深度验证这个 PR", "验证这个 PR 并把报告发到评论", "有更新了,重新验证", "重新验证 PR <n>", "re-verify after update", A/B load-bearing proof against the base build, mock-free harnesses with wire oracles, and targeted gates — producing tmp/pr<n>-verify-<ts>/report.md plus a machine-readable verdict. Designed for the token-free CI verify job; also usable locally.
argument-hint: '<pr-number> [--publish] [--watch] [--repo <owner>/<repo>] [--assets-repo <owner>/<repo>] [--assets-branch <branch>]'
argument-hint: '<pr-number> [--publish] [--no-publish] [--watch] [--repo <owner>/<repo>] [--assets-repo <owner>/<repo>] [--assets-branch <branch>]'
---
# PR Deep Verification
@@ -195,7 +195,10 @@ do not treat a request without those words as a thinner report.
images land; both defaults, and their `$QWEN_VERIFY_ASSETS_*` overrides, are
resolved in the local publish rule below. Only coordinates like these are
defaultable — posting stays a per-invocation request, because it writes to
the PR.
the PR. The one exception: a follow-up round in the same session inherits
the session's current publish consent for the same PR — granted by an
explicit publish request and kept by bare triggers until a round withdraws
it (see **Manual re-verification** below).
`--repo <owner>/<repo>` is still resolved as below: the flag does not license
an `origin` fallback, but a maintainer checkout whose `origin` already is the
intended `owner/repo` is a match to confirm, not a guess to avoid.
@@ -357,8 +360,20 @@ ambiguous rather than verify a guessed PR. Then, in order:
the previous-finding status table, re-measure every carried-forward
finding at `H`.
5. Inherit the previous round's coordinates (`--repo`, assets repo/branch)
instead of asking again. Publishing stays a per-invocation request: the
phrase alone does not post a comment.
instead of asking again. Publish consent is a per-PR state of this
session that the trigger inherits: a round invoked with an explicit
publish request — the `--publish` flag or the request in words —
grants it; a fresh `/verify-pr` round invoked without a publish
request, or a trigger carrying `--no-publish`, withdraws it; a bare
trigger keeps it unchanged, so consecutive bare triggers keep
publishing under the original grant until a round withdraws it.
Watch rounds run under their own standing consent and never change
this state. While consent is granted, publish under it and say so in
one line before posting, naming the round that granted it; the same
line goes into the comment body before the attribution footer.
Without that consent, publishing stays a per-invocation request: the
phrase alone does not post a comment, and a published comment found
on the PR is evidence of a past round, not consent for this one.
To make this trigger automatic instead of typed, install `--watch` (above);
the two share that head-dedup file.
@@ -366,14 +381,17 @@ the two share that head-dedup file.
**Maintainer-driven local publish (only on explicit request).** "Never
post" yields when the maintainer running the local round explicitly asks
for a PR comment; `--publish` in the invocation is that request, and needs
nothing restated. Publish onto an assets branch whose raw.githubusercontent.com
URLs render inline, and resolve both coordinates instead of asking: the
repository is `--assets-repo`, else `$QWEN_VERIFY_ASSETS_REPO`, else the
resolved `--repo`; the branch is `--assets-branch`, else
nothing restated. A follow-up round inherits that request only as **Manual
re-verification** defines: the session's standing consent for the same PR,
granted by an explicit publish request and announced before posting. Publish
onto an assets branch whose raw.githubusercontent.com URLs render inline,
and resolve both coordinates instead of asking: the repository is
`--assets-repo`, else `$QWEN_VERIFY_ASSETS_REPO`, else the resolved
`--repo`; the branch is `--assets-branch`, else
`$QWEN_VERIFY_ASSETS_BRANCH`, else `assets/pr<n>-validation` — the name the
existing assets branches already use — and create it when absent, because the
upload cannot: a contents PUT commits onto an existing ref and answers `422
Branch not found` for a branch that does not exist yet. GET
existing assets branches already use — and create it when absent, because
the upload cannot: a contents PUT commits onto an existing ref and answers
`422 Branch not found` for a branch that does not exist yet. GET
`repos/<assets-repo>/git/ref/heads/<branch>` and, only on a 404, POST
`repos/<assets-repo>/git/refs` with `ref=refs/heads/<branch>` and the head sha
of that repository's default branch — `gh api repos/<assets-repo> --jq
@@ -393,8 +411,10 @@ JSON payload — `-f content="$(base64 …)"` blows past ARG_MAX around 100 KB.
Keep the report.md contract in the comment body: verdict line first — a
plain paragraph, never a `#` heading, which renders the whole verdict as
one giant H1 — collapsed `<details>` Chinese summary immediately after,
images referenced by the same kebab-case names the files carry. End the
comment with the attribution footer, the same shape `/review` posts:
images referenced by the same kebab-case names the files carry. A round
publishing under inherited consent places its one-line announcement
immediately before the footer. End the comment with the attribution footer,
the same shape `/review` posts:
```text
_— <model> via Qwen Code /verify-pr (v<cli-version>; skill <skill-hash>)_
@@ -416,7 +436,8 @@ actually landed. The local path has no publisher enforcing the fail/mismatch
rule, so run `node scripts/verify-pr-check.mjs <artifact dir>` and clear its
errors and its verdict-shape and footer warnings before publishing (a
missing footer is legitimate only when the model could not be resolved),
then state the verdict word from `verdict.txt` verbatim. A published
act on a gates-size warning by recording the suites it ran under `gates`
(with none run, say in the report what the count holds), then state the verdict word from `verdict.txt` verbatim. A published
filename is spent: raw.githubusercontent serves a URL from cache, so
overwriting `02-matrix.png` leaves every earlier link showing the old image.
Republish as `02-matrix-v2.png`, update the body and every comment that quotes
@@ -1335,6 +1356,18 @@ vitest) and cite exact counts. Never claim a repo-wide gate you did not run;
never re-run what the PR's own CI already covers unless your A/B needs the
number from a known-clean state.
**Gate counts are not assertions.** Record each suite under `gates` in
`assertions.json`, as `{"suite": "cli acp-session", "pass": 1292, "fail": 0}`,
never in `pass`, `fail` or `total`. Those count the checks this round wrote.
Folding a suite in turns them into a count of the repo's tests: a round
published "7737 scripted assertions" while holding a few dozen checks of its
own. A mutant run is likewise one assertion (killed, or survived as
predicted), not its suite's test count. So is each suite run: it passes when
green, or when its only failures are the pre-existing ones shown below, and a
gate red the PR causes, green at base and red at head, fails with its
attribution. A round whose only checks are suites thus still records a
positive total, which the checker requires.
**Prove the gate is live before citing it as evidence.** A linter that exits
0 because it matched no files looks exactly like a linter that passed: plant
a violation it must catch (an unused variable, a formatting break), confirm
@@ -1578,7 +1611,9 @@ workflow globs). It must contain:
nonzero add `failures`, one `{id, attribution}` entry per failed assertion,
with `attribution` one of `pr`, `harness` or `environment` — the verdict is
read from that ledger, so a red the PR did not cause cannot decide it (see
**Hard rules**).
**Hard rules**). Existing test suites run as targeted gates go in `gates`,
one `{suite, pass, fail}` entry per suite (`skipped` optional), never in the
three counts (see _Targeted gates_).
- `mutations.json` — the mutation matrix as data, whenever the round ran one:
`rows[]`, and, when the report claims per-commit coverage,
`coverageClaim: "per-commit"` plus `productionCommits` from
@@ -1653,7 +1688,10 @@ head behaviour the PR got wrong. Measured example: a round reproduced a
regression, asserted that the head showed it, and published `blocked` over
4382 passes and 0 failures. The CI publisher runs `verify-pr-check.mjs` before
it trusts any verdict, so run it on the artifact dir before you finish, in
either path.
either path. The publisher reads only the checker's last line and never
echoes warnings, so a gates-size warning reaches nobody but you — record
the suites under `gates` before publishing, or, if the round ran none, say in
the report what the count holds.
### report.md structure
@@ -1797,8 +1835,9 @@ defect.
publish path.** The report is advisory evidence for humans; the workflow
owns publication. The single exception is the maintainer-driven local
publish described in the environment contract, which requires an explicit
request from the maintainer running the round and still never approves,
merges, or closes anything.
request from the maintainer — given in the round's invocation or
inherited within the same session as **Manual re-verification** defines —
and still never approves, merges, or closes anything.
- **Fail loud.** If the environment breaks (build missing, worktree broken),
write `inconclusive` with the exact error rather than improvising a partial
verdict that looks complete.
+240
View File
@@ -12,6 +12,246 @@ are listed; nightly and preview pre-releases are intentionally omitted.
> [GitHub Releases](https://github.com/QwenLM/qwen-code/releases). Do not edit it
> by hand — run `npm run changelog` to regenerate.
## [0.26.6](https://github.com/QwenLM/qwen-code/releases/tag/v0.26.6) - 2026-11-02
### Highlights
_See the complete change list below._
### Breaking Changes
No known breaking changes.
### Complete Change List
#### Features
- Adds a Web Shell sidebar activity view for trusted workspace conversations, showing approval/input/error states, unread completions, running work, and bulk read/archive controls. ([#16022](https://github.com/QwenLM/qwen-code/pull/16022)) by @deggs7
- Adds an "Add to Split View" option to eligible sidebar session menus in web shell, making it easier to open sessions side by side. ([#15956](https://github.com/QwenLM/qwen-code/pull/15956)) by @4ekuct25
- Adds request success rate, output speed, per-model success indicators, and distinct model colors to the Web Shell token usage panel. ([#16108](https://github.com/QwenLM/qwen-code/pull/16108)) by @qqqys
- Web Shell composer shows a dismissible Workflow chip that turns Workflow mode off, warns when tools are unavailable, and summarizes the mode in the + menu. ([#16107](https://github.com/QwenLM/qwen-code/pull/16107)) by @qqqys
- Web Shell Token Usage now plots per-request output speed, first-token latency, and failure details by model, making session performance and errors easier to inspect. ([#16111](https://github.com/QwenLM/qwen-code/pull/16111)) by @qqqys
- With experimental.unifiedAgentLoop enabled, more ACP turns, including channel prompts and Goal-proposal approvals, run on the shared loop. ([#15902](https://github.com/QwenLM/qwen-code/pull/15902)) by @qqqys
- Mods can now run prompt.submit hooks in headless runs for -p prompts and stream-json user messages, allowing prompt inspection or rewriting before submission. ([#15905](https://github.com/QwenLM/qwen-code/pull/15905)) by @qqqys
- Desktop releases now publish Fedora 44 and RHEL 10 x64 RPM installers alongside existing Linux packages. ([#16060](https://github.com/QwenLM/qwen-code/pull/16060)) by @yiliang114
- feat(web-shell): make GitHub channel configuration usable ([#16096](https://github.com/QwenLM/qwen-code/pull/16096)) by @wenshao
- feat(verify-pr): inherit --publish consent on same-session re-verification ([#16112](https://github.com/QwenLM/qwen-code/pull/16112)) by @wenshao
- feat(ci): let the PR review canary cohort carry its own endpoint ([#16130](https://github.com/QwenLM/qwen-code/pull/16130)) by @yiliang114
#### Bug Fixes
- OAuth refresh lock cleanup now removes the lock only when the current process owns it, preventing exiting processes from deleting active refresh locks. ([#15661](https://github.com/QwenLM/qwen-code/pull/15661)) by @2SinceJuly
- MCP tool discovery now skips duplicate tool names but keeps all uniquely named tools, preventing one name collision from hiding an entire server's tools. ([#15757](https://github.com/QwenLM/qwen-code/pull/15757)) by @2SinceJuly
- The Web Shell Plugins page now shows a back control when the navigation rail is unavailable, letting users return to chat from compact layouts. ([#15651](https://github.com/QwenLM/qwen-code/pull/15651)) by @2SinceJuly
- LSP code-navigation queries now fail with an explicit reason when no language server is ready instead of returning empty results. ([#15946](https://github.com/QwenLM/qwen-code/pull/15946)) by @404khai
- The verify report now counts targeted gate test-suite results separately from a round's scripted assertions, making assertion totals reflect only round checks. ([#16013](https://github.com/QwenLM/qwen-code/pull/16013)) by @wenshao
- Makes long /btw answers scroll in a bounded panel with mouse wheel, Shift+Up/Down, PageUp/PageDown, and a scrollbar, while short answers remain unchanged. ([#15982](https://github.com/QwenLM/qwen-code/pull/15982)) by @2SinceJuly
- Fixes verify-pr to count each suite run as one assertion and handle skip notices on closed PRs so review rounds are not rejected with zero assertions. ([#16050](https://github.com/QwenLM/qwen-code/pull/16050)) by @wenshao
- Fixes Kimi K3 generation recognition so supported IDs use the 1,000,000-token context window, while k3-256k keeps its 256K window. ([#16048](https://github.com/QwenLM/qwen-code/pull/16048)) by @wenshao
- Fixes review command so plain-language re-review requests target the intended PR and do not misread resume flags or file-like tokens. ([#15998](https://github.com/QwenLM/qwen-code/pull/15998)) by @wenshao
- Installs DejaVu fonts in the sandbox so capture cards use the intended monospace font and render legibly. ([#16032](https://github.com/QwenLM/qwen-code/pull/16032)) by @yiliang114
- Moves the Workflow mode toggle from the composer toolbar into the composer “+” menu to reduce toolbar clutter while keeping it accessible. ([#16047](https://github.com/QwenLM/qwen-code/pull/16047)) by @qqqys
- Improves managed-agent recovery accuracy by blocking gap fills without child ledger evidence and clarifying unknown or never-started states. ([#16015](https://github.com/QwenLM/qwen-code/pull/16015)) by @wenshao
- Fixes managed-agent child result delivery status so consumed child results are no longer marked pending. ([#16074](https://github.com/QwenLM/qwen-code/pull/16074)) by @wenshao
- Fixes AI attribution notes for loosely spelled git commit --amend commands so they diff against the amended commit, not earlier commits. ([#16073](https://github.com/QwenLM/qwen-code/pull/16073)) by @yiliang114
- Makes loop-protection reminder delivery and telemetry settle only when the model request carrying the reminder is accepted, preventing premature clearing or logging. ([#15559](https://github.com/QwenLM/qwen-code/pull/15559)) by @suzhiguo7
- fix(core): don't treat fd duplication (2>&1) as a file write in shell permission checks ([#15681](https://github.com/QwenLM/qwen-code/pull/15681)) by @Suaroman
- Sanitizes goal approval text and edit-diff file paths in Web Shell by escaping control and bidi characters so displayed approval content cannot carry hidden controls. ([#16080](https://github.com/QwenLM/qwen-code/pull/16080)) by @yiliang114
- Shows Managed Session list failures as a localized Retry notice with request details and avoids the empty state until loading succeeds. ([#16082](https://github.com/QwenLM/qwen-code/pull/16082)) by @yiliang114
- Fixes invalid Javadoc left after the module split so the managed-agent release profile can build documentation again. ([#16099](https://github.com/QwenLM/qwen-code/pull/16099)) by @doudouOUC
- Moves the Web Shell managed session list failure notice below the Sessions / Archived tabs and prevents its request id from being cut off. ([#16104](https://github.com/QwenLM/qwen-code/pull/16104)) by @yiliang114
- Fixes qwen-managed-agent-upgrade start and verify so they reach the control plane and Hosted Harness using the server's resolved endpoint configuration. ([#16051](https://github.com/QwenLM/qwen-code/pull/16051)) by @wenshao
- Tightens the Web Shell mobile composer layout by aligning workspace controls, truncating long names, and centering the context-usage ring. ([#16103](https://github.com/QwenLM/qwen-code/pull/16103)) by @ytahdn
- VS Code companion session history now groups This Week by the current calendar week, so earlier sessions are placed in Older instead of incorrectly appearing under This Week. ([#16097](https://github.com/QwenLM/qwen-code/pull/16097)) by @yiliang114
- The bundled review skill now tells fetch-pr to re-run with both --since and --since-model and removes the retired range model, preventing incomplete anchors. ([#16094](https://github.com/QwenLM/qwen-code/pull/16094)) by @yiliang114
- The Managed runtime release workflow can now rebuild the missing 0.26.5 runtime from its original tag after repairing the Javadoc issue that blocked the release. ([#16114](https://github.com/QwenLM/qwen-code/pull/16114)) by @doudouOUC
- Windows installer updates now stop the bundled runtime before replacing files, reducing update failures when node.exe remains locked. ([#16071](https://github.com/QwenLM/qwen-code/pull/16071)) by @yiliang114
- Channel users can now /approve or /deny their own pending one-time tool permission requests in the current attended turn. ([#16087](https://github.com/QwenLM/qwen-code/pull/16087)) by @BenGuanRan
- Linux AppImage builds now verify required dynamic libraries are bundled or documented as host-provided, helping prevent missing-library failures. ([#16062](https://github.com/QwenLM/qwen-code/pull/16062)) by @yiliang114
- Mods that register prompt.paint or ui.place hooks now load successfully, with those unsupported hooks skipped instead of stopping startup. ([#16124](https://github.com/QwenLM/qwen-code/pull/16124)) by @qqqys
- fix(mcp): warn about disabled tool typos on every discovery path ([#16120](https://github.com/QwenLM/qwen-code/pull/16120)) by @yiliang114
- fix(managed-agent): Restore profile-owned runtime verification ([#16134](https://github.com/QwenLM/qwen-code/pull/16134)) by @doudouOUC
- fix(core): attribute background shell notifications to the registering turn ([#16125](https://github.com/QwenLM/qwen-code/pull/16125)) by @yiliang114
- fix(core): report redirects on cd segments ([#16129](https://github.com/QwenLM/qwen-code/pull/16129)) by @yiliang114
#### Documentation
- Updates daemon usage documentation with current deployment, security, authenticated startup, token sidecar lifecycle, and lifecycle-command guidance. ([#16056](https://github.com/QwenLM/qwen-code/pull/16056)) by @wenshao
- Adds a README quickstart for daemon mode covering qwen serve startup, status/log/stop commands, default loopback binding, and local security warnings. ([#16053](https://github.com/QwenLM/qwen-code/pull/16053)) by @wenshao
- Updates managed-agent structure-review documentation to reflect the Runtime Broker extraction and newly separated recovery, reconciliation, and cancellation components. ([#16109](https://github.com/QwenLM/qwen-code/pull/16109)) by @wenshao
- ManagedAgentStore documentation now reflects the completed store conversion and removes outdated setter descriptions. ([#16118](https://github.com/QwenLM/qwen-code/pull/16118)) by @wenshao
#### Internal Changes
- Public and WebShell Session event streams now share replay and live delivery while keeping existing payloads, event names, and delivery order. ([#15952](https://github.com/QwenLM/qwen-code/pull/15952)) by @doudouOUC
- ci: route the classify_pr gate to ECS for every pull request ([#16019](https://github.com/QwenLM/qwen-code/pull/16019)) by @yiliang114
- Adds an end-to-end test for /autofix-pr watch helpers against real git and a fake gh to improve automated fix reliability. ([#16017](https://github.com/QwenLM/qwen-code/pull/16017)) by @wenshao
- Adds a managed-agent contract test asserting A3 sibling call binding and approval snapshot consistency, with no production behavior change. ([#15995](https://github.com/QwenLM/qwen-code/pull/15995)) by @wang1408
- Makes Windows preview installer tests run reliably through cmd.exe so nightly results are reported correctly. ([#16028](https://github.com/QwenLM/qwen-code/pull/16028)) by @yiliang114
- Improves managed-agent broker reliability by separating runtime execution workflows. ([#16029](https://github.com/QwenLM/qwen-code/pull/16029)) by @doudouOUC
- Makes managed-agent session store initialization safer by using constructor dependency injection. ([#16061](https://github.com/QwenLM/qwen-code/pull/16061)) by @doudouOUC
- test(managed-agent): gate the expired-cursor resync answer ([#16076](https://github.com/QwenLM/qwen-code/pull/16076)) by @wenshao
- Refactors Managed Agent runtime execution to centralize dispatch, cancellation, and completion tracking in an internal dispatcher. ([#16084](https://github.com/QwenLM/qwen-code/pull/16084)) by @doudouOUC
- Adds a Managed Agent test verifying mcp-release refusal and same-id retry behavior when a session uses two MCP servers. ([#16089](https://github.com/QwenLM/qwen-code/pull/16089)) by @wenshao
- Adds a managed-agent test verifying detach is refused while a tool result is unknown and the session remains attached. ([#16098](https://github.com/QwenLM/qwen-code/pull/16098)) by @wenshao
- Reconnects Hosted fault probes to current execution owners so managed-agent tests observe dispatch, cancellation, and delayed replies accurately. ([#16101](https://github.com/QwenLM/qwen-code/pull/16101)) by @doudouOUC
- Settings documentation now matches the runtime schema, and memory.enableAutoSkill and security.auth.useExternal are correctly shown as false. ([#16085](https://github.com/QwenLM/qwen-code/pull/16085)) by @yiliang114
- Windows installation PATH persistence is now protected by a contract test, reducing the chance that a fresh install fails to remain on PATH. ([#16072](https://github.com/QwenLM/qwen-code/pull/16072)) by @yiliang114
- Managed Agent storage was reorganized into Agent and Session stores without changing user-visible behavior. ([#16090](https://github.com/QwenLM/qwen-code/pull/16090)) by @doudouOUC
- refactor(managed-agent): trim store delegation boilerplate ([#16142](https://github.com/QwenLM/qwen-code/pull/16142)) by @doudouOUC
### New Contributors
- @404khai made their first contribution in [#15946](https://github.com/QwenLM/qwen-code/pull/15946)
- @wang1408 made their first contribution in [#15995](https://github.com/QwenLM/qwen-code/pull/15995)
- @suzhiguo7 made their first contribution in [#15559](https://github.com/QwenLM/qwen-code/pull/15559)
- @Suaroman made their first contribution in [#15681](https://github.com/QwenLM/qwen-code/pull/15681)
**Full Changelog**: https://github.com/QwenLM/qwen-code/compare/v0.26.5...v0.26.6
## [0.26.5](https://github.com/QwenLM/qwen-code/releases/tag/v0.26.5) - 2026-11-01
### Highlights
_See the complete change list below._
### Breaking Changes
No known breaking changes.
### Complete Change List
#### Features
- Adds bounded SQL history queries for Managed local H2 and Hosted MySQL, enabling paged recent messages, journal transactions, and exact operation or command lookups. ([#15839](https://github.com/QwenLM/qwen-code/pull/15839)) by @doudouOUC
- Enables Generated UI by default and lets users continue chatting with interactive card values attached as removable context above the composer. ([#15856](https://github.com/QwenLM/qwen-code/pull/15856)) by @qqqys
- feat(web-shell): keyboard shortcuts for Session Overview and Split View ([#14179](https://github.com/QwenLM/qwen-code/pull/14179)) by @4ekuct25
- Adds a Russian locale for the Web Shell Goal card and Goals dialog, with English fallback for untranslated strings. ([#13685](https://github.com/QwenLM/qwen-code/pull/13685)) by @4ekuct25
- Extends Web Shell one-click daemon updates to pnpm, yarn, bun, and sudo-prefixed npm installs, and shows manual steps when automatic update is unavailable. ([#15849](https://github.com/QwenLM/qwen-code/pull/15849)) by @wenshao
- Web Shell and Qwen Code Desktop now show active background agent counts in the status-bar task pill while agents are running. ([#14002](https://github.com/QwenLM/qwen-code/pull/14002)) by @4ekuct25
- Managed WebShell now reads the latest 100 live items directly and pages upward for older messages, while live updates continue. ([#15865](https://github.com/QwenLM/qwen-code/pull/15865)) by @doudouOUC
- The /verify-pr local flow now reuses the current working tree only when it is a clean checkout of the PR head and records round details. ([#15913](https://github.com/QwenLM/qwen-code/pull/15913)) by @wenshao
- Archived conversations in Settings now show Export conversation and Restore inline beside More, making archive management faster. ([#15872](https://github.com/QwenLM/qwen-code/pull/15872)) by @deggs7
- PR watch now merges a stale base before attempting fixes when CI failures may be caused by the branch being behind. ([#15922](https://github.com/QwenLM/qwen-code/pull/15922)) by @wenshao
- Adds an experimental one-command Helm deployment for Managed Agent with Hosted Harness, MySQL, local execution, and an authenticated gateway. ([#15492](https://github.com/QwenLM/qwen-code/pull/15492)) by @doudouOUC
- Adds --in-place mode to the PR-watch skill to co-drive in your checkout, push the branch, and reply on the PR. ([#15929](https://github.com/QwenLM/qwen-code/pull/15929)) by @wenshao
- Keeps skill slash-command argument hints visible while typing and shrinks them to missing arguments. ([#15930](https://github.com/QwenLM/qwen-code/pull/15930)) by @wenshao
- Makes context memory files in the Context Usage panel clickable to open read-only previews such as AGENTS.md. ([#15932](https://github.com/QwenLM/qwen-code/pull/15932)) by @wenshao
- Makes autofix-pr post a pull-request comment when a round is blocked, stating what was attempted, what blocks it, and what human action is needed. ([#15978](https://github.com/QwenLM/qwen-code/pull/15978)) by @wenshao
- Adds a retry budget to /autofix-pr so a check left failing after three fix pushes is handed to a human and shown in /autofix-pr status. ([#15985](https://github.com/QwenLM/qwen-code/pull/15985)) by @wenshao
- Adds a Workflow switch in Web Shell, split panes, and VS Code chat to update saved session Workflow preferences for the next turn without interrupting responses. ([#15779](https://github.com/QwenLM/qwen-code/pull/15779)) by @qqqys
- Enables channel adapters to expose tools to agent sessions and adds QQ moderation tools, including recall, mute, kick, and join approval, controlled by moderationTools. ([#14348](https://github.com/QwenLM/qwen-code/pull/14348)) by @Eric-GoodBoy-Tech
- Adds a batch request and SDK method to fetch live-session snapshots for up to 20 selected workspaces in one call. ([#12513](https://github.com/QwenLM/qwen-code/pull/12513)) by @XIQIXIQIXIQI
- feat(cli): support exact-version updates without npm discovery ([#11486](https://github.com/QwenLM/qwen-code/pull/11486)) by @XIQIXIQIXIQI
- Improves the bundled code-review skill by re-anchoring after head drift, running actionlint checks, accounting for blockers, and refusing retries. ([#16001](https://github.com/QwenLM/qwen-code/pull/16001)) by @wenshao
#### Bug Fixes
- Hardens the managed-agent upgrade command's check, backup, install, start, and verify phases, including cleanup after interrupted installs. ([#15853](https://github.com/QwenLM/qwen-code/pull/15853)) by @wenshao
- TUI CI now runs impact-selected checks and reserves full source TUI acceptance for nightly runs, manual dispatch, and high-risk changes. ([#15864](https://github.com/QwenLM/qwen-code/pull/15864)) by @yiliang114
- Main pushes now run one macOS E2E shard instead of two, while schedules, manual dispatch, and feat/e2e branches keep the two-shard matrix. ([#15873](https://github.com/QwenLM/qwen-code/pull/15873)) by @yiliang114
- The Web Shell floating approval card for tool approvals now uses a single scroll region, avoiding nested scrollbars in long diffs. ([#14621](https://github.com/QwenLM/qwen-code/pull/14621)) by @4ekuct25
- Release builds and eligible downstream jobs now use labelled runners on existing ECS hosts to reduce hosted-runner queue delays. ([#15832](https://github.com/QwenLM/qwen-code/pull/15832)) by @yiliang114
- CUA input, window selection, and session cleanup align with 0.27.0 across CUA SDK, Browser SDK, and Node REPL for more reliable sessions. ([#15784](https://github.com/QwenLM/qwen-code/pull/15784)) by @LaZzyMan
- Review command tests now pin ghApiAllNested pagination and response merging behavior to prevent regressions in GitHub API requests. ([#12560](https://github.com/QwenLM/qwen-code/pull/12560)) by @indexGin
- The No-workspace sessions header now matches Pinned, Workspaces, and Recent with consistent height, muted text, spacing, chevron, and hover/focus behavior. ([#15897](https://github.com/QwenLM/qwen-code/pull/15897)) by @deggs7
- E2E builds now use the same pool-first runner routing and disk gate as other jobs, reducing hosted queue delays for post-merge test signals. ([#15885](https://github.com/QwenLM/qwen-code/pull/15885)) by @yiliang114
- Web Shell browser smoke tests now run on isolated ubuntu-latest runners to avoid shared-runner network failures. ([#15886](https://github.com/QwenLM/qwen-code/pull/15886)) by @yiliang114
- Web Shell now hides the No-workspace sessions sidebar section when it loads empty, matching Pinned behavior. ([#15916](https://github.com/QwenLM/qwen-code/pull/15916)) by @deggs7
- fix(web-shell): keep "+ Add session" clickable when the catalog is empty ([#15835](https://github.com/QwenLM/qwen-code/pull/15835)) by @2SinceJuly
- fix(ci): weight PR owner assignment by open issue load ([#15194](https://github.com/QwenLM/qwen-code/pull/15194)) by @DragonnZhang
- fix(cli): separate instruction files from auto memory in context usage ([#15863](https://github.com/QwenLM/qwen-code/pull/15863)) by @callmeYe
- fix(core): skip memory imports of files already loaded as context files ([#15687](https://github.com/QwenLM/qwen-code/pull/15687)) by @tneemo
- fix(core): retry bodyless 400 responses instead of treating them as fail-fast ([#15821](https://github.com/QwenLM/qwen-code/pull/15821)) by @Andrea-Bruno
- fix(cli): share hosted harness deadlines and bail after CI retries ([#14752](https://github.com/QwenLM/qwen-code/pull/14752)) by @zonemeen
- fix(web-shell): prune split panes whose session left the active catalog ([#14577](https://github.com/QwenLM/qwen-code/pull/14577)) by @4ekuct25
- fix(core): stop the session LSP client on Config shutdown ([#14778](https://github.com/QwenLM/qwen-code/pull/14778)) by @he-yufeng
- Session messaging guidance now explains safe-mode suppression or missing peer addresses without implying recipients do not exist. ([#13948](https://github.com/QwenLM/qwen-code/pull/13948)) by @haomint
- Memory writes now report omitted index entries and scan-cap omissions separately, with sample names and preserved warnings. ([#14937](https://github.com/QwenLM/qwen-code/pull/14937)) by @jabrailkhalil
- Web Shell tool-card diffs now wrap long lines so diffs with long comments or paths scroll vertically instead of showing per-line horizontal scrollbars. ([#13272](https://github.com/QwenLM/qwen-code/pull/13272)) by @ifan855
- /context now displays token counts of one million or more as millions in terminal, plain text, and OpenTUI views. ([#13237](https://github.com/QwenLM/qwen-code/pull/13237)) by @Shizoqua
- Fixed a CLI test-suite build failure by completing the settle-budget rename in hosted harness tests. ([#15935](https://github.com/QwenLM/qwen-code/pull/15935)) by @wenshao
- The sandboxed /verify-pr publisher now runs the artifact check itself and marks the round inconclusive if the check fails or cannot run. ([#15938](https://github.com/QwenLM/qwen-code/pull/15938)) by @wenshao
- Startup probe now separates broker write failures from wrong topic type, giving clearer fail-fast guidance and mqadmin remedy. ([#15941](https://github.com/QwenLM/qwen-code/pull/15941)) by @wenshao
- Fixes /verify-pr publishing to use the newest round and updates skill text to match CI harness-fault behavior. ([#15966](https://github.com/QwenLM/qwen-code/pull/15966)) by @wenshao
- Queues peer-controller registry writes from the same process before taking the cross-process file lock to reduce lock contention and make concurrent updates more reliable. ([#15971](https://github.com/QwenLM/qwen-code/pull/15971)) by @wenshao
- fix(managed-agent): stop a drained pool owner's not-found from retiring automations ([#15990](https://github.com/QwenLM/qwen-code/pull/15990)) by @wenshao
- Routes helper-test temporary files to the runner data-disk temp directory to prevent CI failures caused by a small host /tmp tmpfs. ([#15979](https://github.com/QwenLM/qwen-code/pull/15979)) by @yiliang114
- Exempts non-TUI package trees from the parity and no-flicker gates so PRs touching only them skip those lanes. ([#15937](https://github.com/QwenLM/qwen-code/pull/15937)) by @yiliang114
- Fixes session-commit registration when an absolute cd or pushd resolves to the same repository or a subdirectory, keeping commits tracked. ([#15953](https://github.com/QwenLM/qwen-code/pull/15953)) by @yiliang114
- isAoneHostFamily now uses the shared host normalizer so review remote matching and write routing judge Aone hosts consistently. ([#15934](https://github.com/QwenLM/qwen-code/pull/15934)) by @yiliang114
- fix(vscode): gate the companion vitest worker cap on the shared pool ([#15940](https://github.com/QwenLM/qwen-code/pull/15940)) by @yiliang114
- Peer-controller registry writers now wait up to 8s for a contended lock and can take over a crashed holder's lock without losing writes. ([#15992](https://github.com/QwenLM/qwen-code/pull/15992)) by @wenshao
- The CLI now requests consent for native extension commands before direct installation in OpenTUI; replacement or unmount cancels pending requests. ([#15947](https://github.com/QwenLM/qwen-code/pull/15947)) by @chiga0
- Fixes managed-agent cancellation, Maven embedding, Hosted fault testing, and SDK stream-idle recovery so verification flows behave reliably. ([#15927](https://github.com/QwenLM/qwen-code/pull/15927)) by @doudouOUC
- Makes each /verify round publish and update only its own status comment instead of overwriting other verification comments. ([#15988](https://github.com/QwenLM/qwen-code/pull/15988)) by @wenshao
- Fixes deep-verification reports so the verdict line stays a plain paragraph and the published comment is verified against the intended body. ([#16002](https://github.com/QwenLM/qwen-code/pull/16002)) by @wenshao
- Adds the missing development proxy for autofix-pr routes so the PR watches page reaches the daemon. ([#16004](https://github.com/QwenLM/qwen-code/pull/16004)) by @wenshao
- Updates MCP client and core to 2.4.0 to drop pre-bundled fast-uri 3.1.0 and improve dependency auditability. ([#15925](https://github.com/QwenLM/qwen-code/pull/15925)) by @yiliang114
- Uses the automatic GitHub job token for release publish checkout, reducing authentication failures while keeping PAT for branch push and release creation. ([#15730](https://github.com/QwenLM/qwen-code/pull/15730)) by @qwen-code-dev-bot
- Adds isolation so /verify-pr mutation matrix runs in its own tree and never on live measured cells. ([#15999](https://github.com/QwenLM/qwen-code/pull/15999)) by @wenshao
- Fixes /verify-pr PR badge binding when a flag is glued directly to the PR number, such as 15979--publish. ([#15989](https://github.com/QwenLM/qwen-code/pull/15989)) by @wenshao
- Extends built-in reasoning effort to DeepSeek-V4 and new Qwen prefixes while keeping explicit overrides. ([#15987](https://github.com/QwenLM/qwen-code/pull/15987)) by @wenshao
- Reports a registry that keeps changing during a write as PeerControllerError with code registry-changed and a retry message. ([#16007](https://github.com/QwenLM/qwen-code/pull/16007)) by @wenshao
#### Performance
- Release builds now reuse verified native audio prebuilds from matching successful main releases to skip redundant native builds. ([#15854](https://github.com/QwenLM/qwen-code/pull/15854)) by @yiliang114
#### Documentation
- Managed agent upgrade docs now state that SIGKILL skips cleanup and leaves a temporary upgrade directory that operators should remove. ([#15878](https://github.com/QwenLM/qwen-code/pull/15878)) by @wenshao
- Updated CI documentation to match the actual qwen-autofix job timeout and worst-case budget values. ([#15914](https://github.com/QwenLM/qwen-code/pull/15914)) by @yiliang114
- Adds a bilingual Managed Agent server configuration reference generated from server metadata to keep it current. ([#15939](https://github.com/QwenLM/qwen-code/pull/15939)) by @wenshao
- Adds a bilingual Managed Agent design document with a structure review and staged refactor proposal. ([#15942](https://github.com/QwenLM/qwen-code/pull/15942)) by @wenshao
- Reorganizes the bilingual Managed Agent deployment guide into three tiers for first-time deployments. ([#15944](https://github.com/QwenLM/qwen-code/pull/15944)) by @wenshao
- Adds documentation proposing scheduled /autofix-pr --in-place rounds that report findings and require explicit approval before editing. ([#15960](https://github.com/QwenLM/qwen-code/pull/15960)) by @wenshao
- Updates Managed Agent server docs to link setting tables to the generated configuration reference instead of duplicating settings. ([#15965](https://github.com/QwenLM/qwen-code/pull/15965)) by @wenshao
- Clarifies artifacts.read-timeout documentation to qualify storage stall behavior and retain the per-row lease note. ([#15980](https://github.com/QwenLM/qwen-code/pull/15980)) by @wenshao
- Managed-agent docs now note that private CSI sessions record no committed tool receipts and correct lease wording for artifacts.enabled. ([#16000](https://github.com/QwenLM/qwen-code/pull/16000)) by @wenshao
- Updates managed-agent documentation to describe extracted collaborators and reconciliation prerequisites without changing behavior. ([#16012](https://github.com/QwenLM/qwen-code/pull/16012)) by @wenshao
- Documents the ManagedAgentStore setter investigation and its Spring injection findings. ([#16014](https://github.com/QwenLM/qwen-code/pull/16014)) by @wenshao
#### Internal Changes
- Updates Web Shell smoke tests to use the renamed Add workspace dialog and Workspaces sidebar label. ([#15860](https://github.com/QwenLM/qwen-code/pull/15860)) by @wenshao
- Adds a regression gate for managed-agent failover that verifies recovery when a replacement host loses its first takeover-load reply. ([#15857](https://github.com/QwenLM/qwen-code/pull/15857)) by @wenshao
- Managed public items, transcripts, recent history, and export now use bounded current-message paging instead of persisted snapshots. ([#15870](https://github.com/QwenLM/qwen-code/pull/15870)) by @doudouOUC
- Hosted fault tests now use Managed JDBC stores so identity handling matches production and avoids false test failures. ([#15882](https://github.com/QwenLM/qwen-code/pull/15882)) by @yiliang114
- Web shell test setup now selects the retained radio via keyboard focus and Space, making Generated UI retention checks more stable. ([#15896](https://github.com/QwenLM/qwen-code/pull/15896)) by @yiliang114
- Hosted recovery and transcript fixtures now follow current cancellation, cleanup, and transcript-event contracts for more reliable managed recovery tests. ([#15899](https://github.com/QwenLM/qwen-code/pull/15899)) by @yiliang114
- Web shell recovery tests now include transcript coverage and temporary storage routing so recovered sessions show confirmed answers and latest requests. ([#15883](https://github.com/QwenLM/qwen-code/pull/15883)) by @yiliang114
- Improved managed-agent test reliability by retrying expected 503 projection-pending reads during coordinator restart checks. ([#15910](https://github.com/QwenLM/qwen-code/pull/15910)) by @yiliang114
- Managed tests now count cancellation takeovers only after successful HTTP acceptance while recording all load attempts. ([#15912](https://github.com/QwenLM/qwen-code/pull/15912)) by @yiliang114
- Managed Agent Server is now organized into Maven modules while remaining a single executable service. ([#15871](https://github.com/QwenLM/qwen-code/pull/15871)) by @doudouOUC
- Managed crash-recovery validation now treats targeted cleanup loads as non-resuming recovery while still requiring cancellation takeover. ([#15926](https://github.com/QwenLM/qwen-code/pull/15926)) by @yiliang114
- Adds a failover test verifying Managed Agent recovery when a generation crashes before committing assistant output. ([#15881](https://github.com/QwenLM/qwen-code/pull/15881)) by @wenshao
- Managed Agent session creation now uses a single SessionCreationCommand and fails closed when workspace creation is unsupported. ([#15955](https://github.com/QwenLM/qwen-code/pull/15955)) by @wenshao
- Adds an E2E fault test ensuring Managed Agent recovers correctly when Harness crashes between sibling approvals. ([#15945](https://github.com/QwenLM/qwen-code/pull/15945)) by @wenshao
- Restores classifier-driven web-shell smoke routing to self-hosted ARM64 for admitted runs, with ubuntu-latest fallback for fork PRs. ([#15961](https://github.com/QwenLM/qwen-code/pull/15961)) by @yiliang114
- Adds test evidence and documentation pins for the A3 original-plan count and corrects related prose without changing production behavior. ([#15977](https://github.com/QwenLM/qwen-code/pull/15977)) by @wenshao
- Splits AgentStateStore into six role interfaces so managed-agent components can depend on narrower session-state contracts without behavior changes. ([#15962](https://github.com/QwenLM/qwen-code/pull/15962)) by @wenshao
- Tightens CI checks so the Runtime Broker fault gate only relies on Hosted process setup steps that cannot be skipped or ignored after failure. ([#15964](https://github.com/QwenLM/qwen-code/pull/15964)) by @wenshao
- Adds a CI fault scenario that crashes the managed-agent harness after earlier tool rounds to verify recovery of round, cost, and token state. ([#15981](https://github.com/QwenLM/qwen-code/pull/15981)) by @wenshao
- Adds tests confirming Aliyun OSS tool-publication object open operations retry transient service errors and client timeouts before succeeding. ([#15983](https://github.com/QwenLM/qwen-code/pull/15983)) by @wenshao
- Refactors Runtime Broker by extracting the UNKNOWN-execution lookup cooldown into a dedicated component without changing behavior. ([#15986](https://github.com/QwenLM/qwen-code/pull/15986)) by @wenshao
- Routes the Lint & Static lane to self-hosted ECS runners for all pull requests, including forks, while preserving timeout and override controls. ([#15974](https://github.com/QwenLM/qwen-code/pull/15974)) by @yiliang114
- Fixes Hosted attachment and harness-client lookups for decorated HarnessConnector beans by resolving the underlying connector state. ([#15984](https://github.com/QwenLM/qwen-code/pull/15984)) by @wenshao
- qwen review drive now builds its not-started unavailable report from one factory, keeping zero values and notes unchanged. ([#15943](https://github.com/QwenLM/qwen-code/pull/15943)) by @yiliang114
- Managed-agent recovery tests now verify B2 replacement attempt and stream identities, blanked retracted prefixes, and the live SSE view. ([#15993](https://github.com/QwenLM/qwen-code/pull/15993)) by @wenshao
- The review-salvage replay test now derives the supersede kill-dir prefix from the workflow text, preventing the stub glob from drifting. ([#15963](https://github.com/QwenLM/qwen-code/pull/15963)) by @yiliang114
- Refactors managed-agent runtime hold inventory handling without changing user-visible behavior. ([#15997](https://github.com/QwenLM/qwen-code/pull/15997)) by @wenshao
- Adds tests pinning private CSI receipt guards and applies a follow-up managed-agent design correction. ([#16011](https://github.com/QwenLM/qwen-code/pull/16011)) by @wenshao
- Adds tests covering managed-agent lapsed-lease ledger writes and result-store wiring, with no direct user-facing change. ([#16018](https://github.com/QwenLM/qwen-code/pull/16018)) by @wenshao
### New Contributors
- @indexGin made their first contribution in [#12560](https://github.com/QwenLM/qwen-code/pull/12560)
- @tneemo made their first contribution in [#15687](https://github.com/QwenLM/qwen-code/pull/15687)
- @ifan855 made their first contribution in [#13272](https://github.com/QwenLM/qwen-code/pull/13272)
**Full Changelog**: https://github.com/QwenLM/qwen-code/compare/v0.26.4...v0.26.5
## [0.26.4](https://github.com/QwenLM/qwen-code/releases/tag/v0.26.4) - 2026-10-29
### Highlights
+1
View File
@@ -62,6 +62,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
lsof \
socat \
ca-certificates \
fonts-dejavu-core \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
+21
View File
@@ -135,6 +135,27 @@ asyncio.run(main())
</details>
## Daemon Mode
Run Qwen Code as a long-lived service when you want browser, editor, SDK, and automation clients to share agent sessions without each starting a separate process. The daemon provides the built-in Web Shell and an HTTP + SSE API, supports multiple workspaces, and lets clients reconnect to active sessions.
Start it in the foreground and open the Web Shell:
```bash
qwen serve --open
```
The default address is `http://127.0.0.1:4170`. To run and manage it in the background instead:
```bash
qwen daemon start
qwen daemon status
qwen daemon logs -f
qwen daemon stop
```
By default, the daemon listens on loopback without bearer authentication; any local process that can reach it can use the full API, including code execution. On shared or untrusted hosts, set `QWEN_SERVER_TOKEN` and use `--require-auth`. See the [Daemon guide](./docs/users/qwen-serve.md) for workspaces, authentication, remote access, and the API.
## Capabilities
If you know Claude Code, you already know Qwen Code — and then some. We've put significant effort into [bringing Qwen Code to feature parity with Claude Code](https://github.com/wenshao/codeagents/blob/main/docs/comparison/qwen-code-improvement-report.md), improving both breadth and reliability across the board.
@@ -508,7 +508,7 @@ Focused verification passed 841 core tests and 111 shared-contract tests. The ex
Integration regressions proved that model request preparation failures preserve admitted, unsettled inputs through both the ordinary prompt route and the wake driver, and that interrupted channel settlement confirms the original cleanup owner only after its terminal and file-history retirement. A first release failure retained the cleanup fence; passive resident attachment then released the same binding/generation and added one confirmation without a second terminal. A separate Spring/H2 probe passed the bounded-retraction/replay-floor combination: readers remained usable while the snapshot was rebuilding, and old cursors expired again after snapshot coverage caught up. This probe is not MySQL or process-takeover acceptance.
The machine-readable reports, manifests and retained failed diagnostics are under `.qwen/investigations/g3-step3-implementation/`; the current consolidated report is `packaged-verification.md`. These five packaged gates do not prove the whole matrix above. Since [replaceable-Harness D11](2026-10-02-hosted-replaceable-harness.md#d11--residual-gates-step-3-in-ci-the-model-round-arm-and-q2s-binding-half), they are CI steps of the `hosted-failover-e2e` job, which runs apart from the Hosted MySQL verify (see D11), next to a model-round arm that packages the main observations of B1 for a no-tool Session and of B2, though not their attempt and stream identities, the live SSE view or the admission count.
The machine-readable reports, manifests and retained failed diagnostics are under `.qwen/investigations/g3-step3-implementation/`; the current consolidated report is `packaged-verification.md`. These five packaged gates do not prove the whole matrix above. Since [replaceable-Harness D11](2026-10-02-hosted-replaceable-harness.md#d11--residual-gates-step-3-in-ci-the-model-round-arm-and-q2s-binding-half), they are CI steps of the `hosted-failover-e2e` job, which runs apart from the Hosted MySQL verify (see D11), next to a model-round arm that packages the main observations of B1 for a no-tool Session and of B2 — including, since [B2 stream identities](2026-10-31-hosted-b2-stream-identities.md), the attempt and stream identities and the live SSE view; the same audit also pins the admission count.
#14481's packaged gates add fourteen fault arms to the same job. Seven landed with
the second step. Five layer one fault on the Allow path: three crash the
@@ -560,12 +560,21 @@ restore the turn cost snapshot and per-model token ledger rather than restart th
with each earlier execution occurring once and the later request reissued deep-equal
([B3 round and cost budget](2026-10-31-hosted-b3-round-cost-budget.md)). "Cost budget"
here is the TurnBudget snapshot plus the token ledger, not a cumulative spend gate.
The streamed-prefix identities and the live SSE view (B2) are packaged too: the
same model-round arm audits three distinct attempt ids, pairwise-distinct content
Parts under one shared assistant item, blanked retracted prefixes in reload and
replay, and a live SSE capture whose converged text, answer identity and single
terminal equal reload and replay
([B2 stream identities](2026-10-31-hosted-b2-stream-identities.md)).
Malformed or foreign refs, snapshot overflow, the rewritten-input and
Hook-no-rerun half of A3 (a commandStep follow-up, since a native continuation
refuses Hook/MCP sessions), Hook/MCP/child
uncertainty (A6), the attempt and stream identities plus the live SSE view
(B2), and the mixed-version reader/bundle
closure (R2) stay focused or integration evidence. Full-owner restart, real-provider
uncertainty (A6), and the mixed-version reader/bundle
closure (R2) stay focused or integration evidence. A6's focused evidence has
landed: the Hook/MCP saved-batch ownership refusal and the ledger-less agent
gap-fill block are pinned by behavioural tests
([A6 uncertainty evidence](2026-10-31-hosted-a6-uncertainty-evidence.md)); the
row remains focused rather than packaged. Full-owner restart, real-provider
acceptance and a real mixed-version cluster remain separate. Q2's binding
half has the D11 frozen control-plane arm for a woken former Spring's stale
claim and a store test of the bind guards; two live control planes remain a
@@ -414,9 +414,9 @@ Allow 保留原 Action、prepare key、request digest 和 Runtime binding,产
集成回归证明:模型请求准备失败时,普通 prompt 路由和 wake driver 均保留已准入、未结算的输入;渠道中断结算仅在终态与 file-history 退役之后确认原清理 owner。首次释放失败时保留清理 fence,随后 passive resident attachment 释放相同 binding/generation,只新增一次确认,不重复终态。独立 Spring/H2 probe 通过有界撤回与 replay floor 的组合:snapshot 重建期间 reader 保持可用,覆盖范围追平后旧 cursor 再次过期。这项 probe 不属于 MySQL 或进程接管验收。
机器可读报告、manifest 和保留的失败诊断位于 `.qwen/investigations/g3-step3-implementation/`,当前汇总报告为 `packaged-verification.md`。这五种打包门禁不证明上述整个矩阵。自[可替换 Harness 设计 D11](2026-10-02-hosted-replaceable-harness.zh-CN.md#d11--残留门禁step-3-进-ci模型轮支路q2-的-binding-半边) 起,它们是 `hosted-failover-e2e` 任务的 CI 步骤,该任务独立于 Hosted MySQL 的 verify 运行(见 D11);同一任务里还有一条模型轮支路,把无工具 Session 的 B1 与 B2 的主要观察项做成打包门禁,但不覆盖它们的 attempt 与 stream 身份、实时 SSE 视图以及准入次数。
机器可读报告、manifest 和保留的失败诊断位于 `.qwen/investigations/g3-step3-implementation/`,当前汇总报告为 `packaged-verification.md`。这五种打包门禁不证明上述整个矩阵。自[可替换 Harness 设计 D11](2026-10-02-hosted-replaceable-harness.zh-CN.md#d11--残留门禁step-3-进-ci模型轮支路q2-的-binding-半边) 起,它们是 `hosted-failover-e2e` 任务的 CI 步骤,该任务独立于 Hosted MySQL 的 verify 运行(见 D11);同一任务里还有一条模型轮支路,把无工具 Session 的 B1 与 B2 的主要观察项做成打包门禁——自 [B2 流身份](2026-10-31-hosted-b2-stream-identities.zh-CN.md) 起也包括 attempt 与 stream 身份及实时 SSE 视图;同一审计还钉住准入次数。
#14481 的打包门禁在同一任务里新增十四条故障支路,其中七条随第 2 步落地。五条在 Allow 路径上叠加一层故障:三条在持久 A5 屏障之后崩溃替代 Harness——一条在 Allow 决策落库后(A5 的决策行),一条在批准执行到达 `PREPARED`、Runtime Broker 的 `:start` 被扣住时(A5a 的部分 prepare),还有一条在批准结果已提交、post-tool 模型轮被扣住时(A5 的 result commit 屏障)——另两条在提交后丢弃控制面的回复:决策回复丢失后,同一 `Idempotency-Key` 重试必须落到同一条记录,只产生一个 `ACTION_RESPONSE` operation(A4 的决策半边);Turn 准入回复丢失后,重试只产生一行 Turn(C4)。五条都保留原 Action、prepare key、digest 与 Runtime 身份,启动第三代 Harness,并要求一次已结算执行、一次文件效果和一个终态;result-commit 支路还要求重发复用同一个已保存请求,并撤回已发布的片段。另外五条覆盖接下来的屏障:`turn.settled` 已提交但回复丢失时,替换后必须投影一次终态(不要求第二次 store 提交),且不产生推理或工具调用,公开文本仍必须是恢复出的答案(C1);终态写入被拒时,可付 checkpoint 必须由替换方恰好补偿一次,且恢复出的答案仍可见(C2)。清理释放被拒、随后再次崩溃(C3)也已打包:Turn 进入终态且其 Harness 代际已死之后,控制面通过 cleanup-only load 重试欠付清理,直到替换方确认([清理欠债重试](2026-10-28-hosted-cleanup-debt-retry.zh-CN.md))。接管 load 回复丢失(A4 的 load 半边)也已打包:替换方第一次 drive load 的回复被丢失一次(测试门控钩子),控制面把同一 load 重发进已启动的 active driver,原 Turn 仍须恰好投影一个终态并保留恢复出的答案——重发原样重述 active attachment,而不是把 Turn 卡死。早先观察到的卡 RUNNING 变体在当前代码树上复现不出,因此该臂作为回归门禁守住这条交接,而非标记一个未决缺口。主流 Finished 持久标记与 assistant 提交之间的崩溃(B3a)也已打包:测试门控钩子在替换方的 post-tool 轮记录 Finished 之后、assistant 提交之前把它扣在该窗口内,runner 在那里崩溃它,下一代必须 deep-equal 重发已保存请求并投影一个终态、保留恢复出的答案,而不是补偿一个已提交的输出。混合兄弟审批裂缝中的崩溃(A3 的 native 半边)也已打包:一个两调用批次按兄弟逐个审批,测试门控钩子在较早兄弟已决之后、替换方第一次询问审批时把它扣住,runner 在那里崩溃它,下一代必须用原 deadline 重挂原 pending Action、要求该兄弟自己的 Allow 而非继承较早的 Allow、不重问已决兄弟,并按原顺序各结算两个调用一次([A3 混合兄弟审批](2026-10-31-hosted-a3-mixed-sibling-approval.zh-CN.md))。若干先前工具轮之后的崩溃(B3)也已打包:一个多轮夹具跑两个先前工具,替换方在较后的 post-tool 请求被扣住时崩溃,下一代必须续跑已保存的逻辑轮次、恢复回合成本快照与每模型 token 账本而非重启它们,每个先前执行恰好一次、较后请求 deep-equal 重发([B3 轮次与成本预算](2026-10-31-hosted-b3-round-cost-budget.zh-CN.md))。此处「成本预算」指 TurnBudget 快照加 token 账本,而非累计花费闸门。损坏/外部 ref、快照超限、A3 的改写输入与 Hook 不重跑半边(commandStep 后续,因为 native continuation 拒绝 Hook/MCP 会话)、Hook/MCP/child 不确定性(A6)、attempt 与 stream 身份及实时 SSE 视图(B2),以及混版本 reader/bundle 闭包(R2)仍由定向或集成证据覆盖。两侧 owner 重启、真实 provider 和真实混版本集群仍独立处理。Q2 的 binding 半边有 D11 的冻结控制面支路覆盖醒来的旧 Spring 的过期 claim,并有 store 测试钉住 bind 守卫;两个存活控制面仍属后续阶段。
#14481 的打包门禁在同一任务里新增十四条故障支路,其中七条随第 2 步落地。五条在 Allow 路径上叠加一层故障:三条在持久 A5 屏障之后崩溃替代 Harness——一条在 Allow 决策落库后(A5 的决策行),一条在批准执行到达 `PREPARED`、Runtime Broker 的 `:start` 被扣住时(A5a 的部分 prepare),还有一条在批准结果已提交、post-tool 模型轮被扣住时(A5 的 result commit 屏障)——另两条在提交后丢弃控制面的回复:决策回复丢失后,同一 `Idempotency-Key` 重试必须落到同一条记录,只产生一个 `ACTION_RESPONSE` operation(A4 的决策半边);Turn 准入回复丢失后,重试只产生一行 Turn(C4)。五条都保留原 Action、prepare key、digest 与 Runtime 身份,启动第三代 Harness,并要求一次已结算执行、一次文件效果和一个终态;result-commit 支路还要求重发复用同一个已保存请求,并撤回已发布的片段。另外五条覆盖接下来的屏障:`turn.settled` 已提交但回复丢失时,替换后必须投影一次终态(不要求第二次 store 提交),且不产生推理或工具调用,公开文本仍必须是恢复出的答案(C1);终态写入被拒时,可付 checkpoint 必须由替换方恰好补偿一次,且恢复出的答案仍可见(C2)。清理释放被拒、随后再次崩溃(C3)也已打包:Turn 进入终态且其 Harness 代际已死之后,控制面通过 cleanup-only load 重试欠付清理,直到替换方确认([清理欠债重试](2026-10-28-hosted-cleanup-debt-retry.zh-CN.md))。接管 load 回复丢失(A4 的 load 半边)也已打包:替换方第一次 drive load 的回复被丢失一次(测试门控钩子),控制面把同一 load 重发进已启动的 active driver,原 Turn 仍须恰好投影一个终态并保留恢复出的答案——重发原样重述 active attachment,而不是把 Turn 卡死。早先观察到的卡 RUNNING 变体在当前代码树上复现不出,因此该臂作为回归门禁守住这条交接,而非标记一个未决缺口。主流 Finished 持久标记与 assistant 提交之间的崩溃(B3a)也已打包:测试门控钩子在替换方的 post-tool 轮记录 Finished 之后、assistant 提交之前把它扣在该窗口内,runner 在那里崩溃它,下一代必须 deep-equal 重发已保存请求并投影一个终态、保留恢复出的答案,而不是补偿一个已提交的输出。混合兄弟审批裂缝中的崩溃(A3 的 native 半边)也已打包:一个两调用批次按兄弟逐个审批,测试门控钩子在较早兄弟已决之后、替换方第一次询问审批时把它扣住,runner 在那里崩溃它,下一代必须用原 deadline 重挂原 pending Action、要求该兄弟自己的 Allow 而非继承较早的 Allow、不重问已决兄弟,并按原顺序各结算两个调用一次([A3 混合兄弟审批](2026-10-31-hosted-a3-mixed-sibling-approval.zh-CN.md))。若干先前工具轮之后的崩溃(B3)也已打包:一个多轮夹具跑两个先前工具,替换方在较后的 post-tool 请求被扣住时崩溃,下一代必须续跑已保存的逻辑轮次、恢复回合成本快照与每模型 token 账本而非重启它们,每个先前执行恰好一次、较后请求 deep-equal 重发([B3 轮次与成本预算](2026-10-31-hosted-b3-round-cost-budget.zh-CN.md))。此处「成本预算」指 TurnBudget 快照加 token 账本,而非累计花费闸门。流式前缀身份与实时 SSE 视图(B2)也已打包:同一条模型轮支路审计三个互异 attemptId、唯一共享 assistant item 下逐代互异的 content Part、reload 与 replay 中被清空的撤回前缀,以及收敛文本、答案身份与单终态都等于 reload/replay 的 live SSE 捕获([B2 流身份](2026-10-31-hosted-b2-stream-identities.zh-CN.md))。损坏/外部 ref、快照超限、A3 的改写输入与 Hook 不重跑半边(commandStep 后续,因为 native continuation 拒绝 Hook/MCP 会话)、Hook/MCP/child 不确定性(A6),以及混版本 reader/bundle 闭包(R2)仍由定向或集成证据覆盖。A6 的定向证据已落地:Hook/MCP 保存批次所有权拒绝与无账本 agent 缺口填充阻塞已由行为化测试钉住([A6 不确定性证据](2026-10-31-hosted-a6-uncertainty-evidence.zh-CN.md));该行仍属定向而非打包门禁。两侧 owner 重启、真实 provider 和真实混版本集群仍独立处理。Q2 的 binding 半边有 D11 的冻结控制面支路覆盖醒来的旧 Spring 的过期 claim,并有 store 测试钉住 bind 守卫;两个存活控制面仍属后续阶段。
## 10. 实现范围
@@ -226,11 +226,13 @@ is never vacuous against a history-dropping endpoint.
| `webshell-monitor-resume` | Same over the WebShell stream | Same assertions |
| `cursor-expired-resync` | Resume with a cursor older than the replay window | A single `agent.session.resync_required` frame, then close; no fabricated replay |
`cursor-expired-resync` is deferred out of the slice-1 letter: the replay
`cursor-expired-resync` was deferred out of the slice-1 letter: the replay
floor only advances when a session snapshot lands, so an honestly expired
cursor needs the replay-floor mechanics driven to that point first; the
case lands with slice 2, and its mutation (2 below) moves with it. The
shipped FG8e letter covers the four abort/resume cases.
cursor needs the replay-floor mechanics driven to that point first. It was
later delivered in FG9e
(`docs/design/2026-10-15-session-relay-fault-gates.md`) as the cell carried
by #15750, and its mutation moved with it. The shipped FG8e letter covers
the four abort/resume cases.
A fixture note that cost a flake: the 60 s events poll/heartbeat interval
(which forces the live hub to carry delivery) can hold a WebShell resume
@@ -364,9 +366,9 @@ the merged #14021 / #13986 / #13918 semantics when they land.
1. FG8e — the public replay's cursor tiebreak weakened to include the
cursor row: `public-run-resume` red (duplicate at the resume boundary).
2. Deferred with its case: FG8e — the expired-cursor branch streaming from
sequence 0 instead of answering `resync_required`:
`cursor-expired-resync` red. Lands with slice 2.
2. Deferred with its case; delivered in FG9e as #15750's residual cell:
FG8e — the expired-cursor branch streaming from sequence 0 instead of
answering `resync_required`: `cursor-expired-resync` red.
3. FG8e — the WebShell replay window off by one at the `afterSequence`
boundary: `webshell-run-resume` red (gap or duplicate at the boundary).
4. FG8f — the resume watermark dropped to 0 (resume re-arms only once the
@@ -105,7 +105,7 @@ driver 在 broker 与 store 监听器前内嵌反向代理;lose-once 故障转
| `webshell-monitor-resume` | 在 WebShell 流上做同样注入 | 同样的断言 |
| `cursor-expired-resync` | 以早于重放窗口的游标续传 | 单帧 `agent.session.resync_required`,然后关闭;无伪造重放 |
`cursor-expired-resync` 暂缓、不进片 1 的字母:重放地板只有会话快照落地才会前移,所以一个诚实的过期游标需要先把重放地板机制驱动到那个点;该用例随片 2 落地,它的变异(下面的第 2 条)一并移动。本次交付的 FG8e 字母覆盖四个中止/续传用例。
`cursor-expired-resync` 暂缓、不进片 1 的字母:重放地板只有会话快照落地才会前移,所以一个诚实的过期游标需要先把重放地板机制驱动到那个点。该用例后来在 FG9e(`docs/design/2026-10-15-session-relay-fault-gates.md`)作为 #15750 承接的格子交付,它的变异也一并移动。本次交付的 FG8e 字母覆盖四个中止/续传用例。
一条付出了一个 flake 的 fixture 教训:60 s 的事件轮询/心跳间隔(为强制 live hub 承载投递而设)可把 WebShell 续传压住一个完整轮询周期,而 monitor watch 的空闲窗口必须长过它 —— 空闲超时设 30 s 时,watch 在断流中途把顶在门边的生产者空闲杀掉,用例就吊死在一扇永不再开的门上。watch 以 `idle_timeout_ms` = 180 s 运行。同一轮询也限制续传 observer 追平已提交台账的速度 —— 在重放读取与 relay 停止之间提交的行要晚一个轮询周期才到,所以追平等待设为 120 s,长过两个周期。第二条纪律:monitor producer 绝不自然退出 —— 打过最后一个 mark 后空转,配额停表的 kill 是唯一出路。自然退出会关闭作用域,其 cancel 写入与配额结算赛跑,胜出时把记录翻折成 `stop_requested` 而不是 `max_events`(lane 上实测两次)。
@@ -136,7 +136,7 @@ publisher 住在 Harness 进程内,所以「杀掉 publisher」是一次只断
每个变异单独应用、证明恰好把它自己的门禁打红、然后回滚,沿用 FG6/FG7 记录在案的手动程序。下面的片 1 变异随本设计一起提交;片 2 与片 3 在落地时对着已合入的 #14021 / #13986 / #13918 语义细化自己的变异。
1. FG8e —— 公开流重放的游标 tiebreak 被削弱为包含游标行:`public-run-resume` 红(续传边界处重复)。
2. 随用例一并暂缓:FG8e —— 过期游标分支从序列 0 起播而不是回答 `resync_required`:`cursor-expired-resync` 红。随片 2 落地。
2. 随用例一并暂缓、后在 FG9e 作为 #15750 的残余格子交付:FG8e —— 过期游标分支从序列 0 起播而不是回答 `resync_required`:`cursor-expired-resync` 红。
3. FG8e —— WebShell 重放窗口在 `afterSequence` 边界差一:`webshell-run-resume` 红(边界处缺失或重复)。
4. FG8f —— 恢复水位降为 0(resume 只在缓冲完全排空后才重新武装):`in-flight-cap` 红 —— 生产者停摆后的第一个块要等整批滞留积压全部提交才会落地,恢复时点的已提交数因此到达停摆点而不是停在它后面。
5. FG8f —— journal 提交的重试铸了新的事务身份而非重驱原事务:`receipt-drop-after-watermark` 红(重驱的结算提交与已落库的原提交冲突,记录永不结算)。
@@ -314,22 +314,25 @@ Mechanics notes recorded at landing:
### FG9e — SSE gap (slice 2, delivered)
Two cases instead of the draft's four: each observes one `*` broadcast on
one surface from both of its ends — the sender (alice) and a recipient
(bob) — because both ends ride the same relay and the same team boot,
and the fork budget (#14148) pays for every team boot. The public and
Two resume cases instead of the draft's four: each observes one `*`
broadcast on one surface from both of its ends — the sender (alice) and a
recipient (bob) — because both ends ride the same relay and the same team
boot, and the fork budget (#14148) pays for every team boot. The public and
WebShell streams are served by the in-JVM Spring; the injection is the
client's own abort (its HttpClient's `shutdownNow`) and resume, so the
letter needs no proxy. Each stream is cut only once its client holds
every committed event, resumes only after JDBC proves events committed
beyond its cursor, and the client's whole view (the cut frames, then the
resumed ones) must be one sequence from 1 with no gap and no duplicate,
frame for frame the committed row as that surface serves it.
frame for frame the committed row as that surface serves it. A third
case, `cursor-expired-resync`, arms the expired-cursor answer the draft
deferred (open question 3).
| Case | Injection | Expect |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `public-resume` | Hold the relay; open alice's and bob's public streams; cut alice's once her command is `planned`, then release the relay, so the whole relay runs while she is disconnected; cut bob's once both `msgc_` are minted and before his delivery; let every delivery settle; resume both with `Last-Event-ID` | Nothing relayed before alice's cut (no `msgc_`) and nothing on bob's journal at his; both Sessions committed events beyond their cursors while cut; both resumed views continuous and exactly the ledger (snake_case, second `created_at`); alice's Turn terminal once and her post-relay answer once; bob's launch Turn terminal once |
| `webshell-resume` | Same over the WebShell stream, resumed with `afterSequence` | Same over the WebShell envelope (camelCase, millisecond `createdAt`) |
| Case | Injection | Expect |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `public-resume` | Hold the relay; open alice's and bob's public streams; cut alice's once her command is `planned`, then release the relay, so the whole relay runs while she is disconnected; cut bob's once both `msgc_` are minted and before his delivery; let every delivery settle; resume both with `Last-Event-ID` | Nothing relayed before alice's cut (no `msgc_`) and nothing on bob's journal at his; both Sessions committed events beyond their cursors while cut; both resumed views continuous and exactly the ledger (snake_case, second `created_at`); alice's Turn terminal once and her post-relay answer once; bob's launch Turn terminal once |
| `webshell-resume` | Same over the WebShell stream, resumed with `afterSequence` | Same over the WebShell envelope (camelCase, millisecond `createdAt`) |
| `cursor-expired-resync` | Let the lead's Turn settle, then advance the Session's replay floor through projected message coverage; reconnect both surfaces at cursor 0 and read the JSON route at 0 and at the floor | Exactly one `agent.session.resync_required` frame per surface, with no event id, the floor and a snapshot cursor at or above it, and no partial replay; `GET /events?after=0` answers `409 cursor_expired` with the same floor; `after=<floor>` answers 200 |
Mechanics notes recorded at landing:
@@ -343,11 +346,16 @@ Mechanics notes recorded at landing:
resumes with no gap and no duplicate, frame for frame its ledger. The
sender's end is the relay-dependent one: her Turn's tail commits only
after the relay answered.
- The expired-cursor `resync_required` arm (open question 3) is not
armed: the resync answer belongs to the Session stream's replay floor,
whichever capability produced the events, and the FG8 tracker
(#14163) owns that one case (`cursor-expired-resync`), which its
design deferred and its slice 2 (#14334) has not yet delivered.
- The expired-cursor `resync_required` arm (open question 3) is armed
as `cursor-expired-resync`. The resync answer belongs to the Session
stream's replay floor whichever capability produced the events, so the
case needs no relay traffic of its own: it lets the lead's Turn settle,
advances the floor through projected coverage (the scheduled advancer
is off), and requires both surfaces to answer a cursor below it with
exactly one frame carrying no id, the floor and a snapshot cursor at or
above it. The JSON route answers the same window as
`409 cursor_expired`, and a cursor at the floor stays readable. The
case closes the FG8 tracker's (#14163) deferred cell carried by #15750.
### FG9b — store failures (slice 3, delivered)
@@ -513,8 +521,8 @@ and FG9e's stream heartbeat (below).
- the **relay hold** is the H4e-b3 one: `SchedulerHold` parks a task on
the one-thread `messageRelayScheduler`, so no scan — and with it no
claim or handover — runs until the case releases it; FG9d's
`cancel-after-mint` / `cancel-reply-lost` and both FG9e cases take it
before alice sends;
`cancel-after-mint` / `cancel-reply-lost` and both FG9e resume cases
take it before alice sends;
- a **second proxy instance** fronts the public API for the client's own
cancel POST in `cancel-reply-lost` only (everything else talks to
Spring directly), with the same drop-after-commit disposition;
@@ -722,6 +730,16 @@ source:
sent" while her entry stays live, the relay delivers it anyway, and
the copied-back reply is never consumed.
Residual round (#15750), run against its case alone
(`-Dqwen.fg9e.case=cursor-expired-resync`); the mutation patches the
Java source:
15. FG9e — `replayableEvents` skips the replay-floor check (an expired
cursor replays from sequence 0): `cursor-expired-resync` red — the
stream replays the ledger from its start and never sends the resync
frame, and the JSON route answers 200 instead of `409
cursor_expired`.
## Validation and acceptance
- `npm run build && npm run typecheck`; ESLint clean on the driver;
@@ -732,9 +750,9 @@ source:
counter, `fg9b_fire` count and held kill point observed — per-letter
selectors working, and the happy path (a broadcast with no injection)
green with exactly one `msgc_` row per position.
- Every slice-1 and slice-2 mutation above individually red; source
restored; the gate green again. Slice 3 repeats the procedure for its
mutations.
- Every slice-1, slice-2 and residual-round mutation above individually
red; source restored; the gate green again. Slice 3 repeats the
procedure for its mutations.
- `hosted-process-ci.test.js` green with any moved budgets, and
`check-failsafe-reports.js hosted` satisfied after a full lane run.
- Two consecutive clean open-ended and reverse audit rounds per
@@ -753,7 +771,8 @@ source:
2. Whether the broadcast roster should grow to four members (three
recipients) to sharpen the partial arms' decisiveness — cost is one
more scripted member per case; decided at implementation.
3. Resolved at slice 2: FG9e does not arm the expired-cursor
`resync_required` case. The resync answer belongs to the Session
stream's replay floor whichever capability produced the events, and
the FG8 tracker (#14163) still owes that one case.
3. Closed by the residual round (#15750): FG9e now arms the
expired-cursor `resync_required` case (`cursor-expired-resync`). The
resync answer belongs to the Session stream's replay floor whichever
capability produced the events; the FG8 tracker (#14163) deferred the
cell and #15750 carried it.
@@ -268,19 +268,21 @@ axis 探针把经 proxy 的 warm/createOrLoad 链的 resident-vs-attach 冲突
### FG9e —— SSE 断流(切片 2,已交付)
用两个用例取代草稿的四个:每个用例在一个表面上从两端观察同一次 `*`
用两个续传用例取代草稿的四个:每个用例在一个表面上从两端观察同一次 `*`
广播 —— 发送方(alice)与一名收件人(bob)—— 因为两端搭的是同一个 relay、
同一次团队引导,而 fork 预算(#14148)为每次团队引导付费。公开流与
WebShell 流由 in-JVM Spring 提供;注入就是客户端自己的断开(其
HttpClient 的 `shutdownNow`)与续传,所以本字母不需要 proxy。每条流只在
客户端已持有全部已提交事件后才切断,只在 JDBC 证明游标之后已有事件提交后才
续传,客户端的完整视图(切断前的帧加续传后的帧)必须是从 1 起无缺口、无
重复的一条序列,逐帧等于该表面所提供的已提交行。
重复的一条序列,逐帧等于该表面所提供的已提交行。第三个用例
`cursor-expired-resync` 承接草稿延后的过期游标应答(开放问题 3)。
| 用例 | 注入 | 期望 |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `public-resume` | 扣住 relay;打开 alice 与 bob 的公开流;alice 的命令 `planned` 后切断她的流,再放行 relay,使整个 relay 在她断线期间运行;两个 `msgc_` 都铸出后、bob 的投递之前切断他的流;等全部投递落账;两条流都按 `Last-Event-ID` 续传 | alice 切断时尚无任何 relay(无 `msgc_`),bob 切断时他的 journal 上什么都没有;两个 Session 都在断线期间提交了游标之后的事件;两份续传视图都连续且恰为台账(snake_case、秒级 `created_at`);alice 的 Turn 终态一次、她 relay 之后的回答一次;bob 的启动 Turn 终态一次 |
| `webshell-resume` | 同上,走 WebShell 流,按 `afterSequence` 续传 | 对 WebShell 信封做同样断言(camelCase、毫秒级 `createdAt`) |
| 用例 | 注入 | 期望 |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `public-resume` | 扣住 relay;打开 alice 与 bob 的公开流;alice 的命令 `planned` 后切断她的流,再放行 relay,使整个 relay 在她断线期间运行;两个 `msgc_` 都铸出后、bob 的投递之前切断他的流;等全部投递落账;两条流都按 `Last-Event-ID` 续传 | alice 切断时尚无任何 relay(无 `msgc_`),bob 切断时他的 journal 上什么都没有;两个 Session 都在断线期间提交了游标之后的事件;两份续传视图都连续且恰为台账(snake_case、秒级 `created_at`);alice 的 Turn 终态一次、她 relay 之后的回答一次;bob 的启动 Turn 终态一次 |
| `webshell-resume` | 同上,走 WebShell 流,按 `afterSequence` 续传 | 对 WebShell 信封做同样断言(camelCase、毫秒级 `createdAt`) |
| `cursor-expired-resync` | 让 lead 的 Turn 结束,再按消息投影覆盖推进 Session 的重放地板;两个表面都用游标 0 重连,并在 0 与地板处各读一次 JSON 路由 | 每个表面恰好一帧 `agent.session.resync_required`,不带事件 id,带地板与不低于它的快照游标,且没有任何部分重放;`GET /events?after=0` 以同一地板返回 `409 cursor_expired`;`after=<地板>` 返回 200 |
落地时记下的机制注记:
@@ -290,10 +292,13 @@ HttpClient 的 `shutdownNow`)与续传,所以本字母不需要 proxy。每
恰好出现一次」无事件可数;收件人这一端改为钉住:一条跨过其投递而被切断的流
续传后无缺口、无重复,逐帧等于它的台账。发送方一端才是依赖 relay
的那端:她的 Turn 尾部只在 relay 应答之后才提交。
- 过期游标 `resync_required` 臂(开放问题 3)不武装:resync 应答属于
Session 流的重放地板,与事件由哪个能力产生无关,FG8 的跟踪 issue
(#14163)拥有那唯一的用例(`cursor-expired-resync`)—— 它的设计把该用例
延后,其切片 2(#14334)尚未交付。
- 过期游标 `resync_required` 臂(开放问题 3)以 `cursor-expired-resync`
武装。resync 应答属于 Session 流的重放地板,与事件由哪个能力产生无关,
所以该用例不需要自己的 relay 流量:它让 lead 的 Turn 结束,按消息投影
覆盖推进地板(定时推进器关闭),并要求两个表面都对低于地板的游标只回
一帧 —— 不带 id,带地板与不低于它的快照游标。JSON 路由以同一窗口返回
`409 cursor_expired`,而地板本身仍是可读游标。该用例关闭 FG8 跟踪 issue
(#14163)延后、由 #15750 承接的那一格。
### FG9b —— 存储故障(切片 3,已交付)
@@ -430,7 +435,7 @@ mint 快速逼出 relay 侧超时加重试;另有 FG9e 的流心跳(见下
- **relay 闩锁**沿用 H4e-b3:`SchedulerHold` 在单线程的
`messageRelayScheduler` 上停放一个任务,于是在用例放行之前不会跑任何
scan —— 也就没有认领、没有 handover;FG9d 的 `cancel-after-mint` /
`cancel-reply-lost` 与两个 FG9e 用例都在 alice 发送前拿到它;
`cancel-reply-lost` 与两个 FG9e 续传用例都在 alice 发送前拿到它;
- **第二个 proxy 实例**只为 `cancel-reply-lost` 里客户端自己的 cancel
POST 挡在公开 API 前(其余一律直连 Spring),处置同为提交后丢答;
- **观察者**是 FG8e 观察者的 in-JVM 移植:每条流一个 HttpClient
@@ -592,6 +597,13 @@ relay 那一个修补 Java 源码:
红 —— alice 被告知「未发送」而她的条目仍然有效,relay 照样投递它,回写的
reply 永远不被消费。
残余轮(#15750)单独跑其用例
(`-Dqwen.fg9e.case=cursor-expired-resync`);变异改 Java 源码:
15. FG9e —— `replayableEvents` 跳过重放地板检查(过期游标从序列 0 起播):
`cursor-expired-resync` 红 —— 流从台账开头重放且永不发送 resync 帧,
JSON 路由返回 200 而非 `409 cursor_expired`。
## 验证与验收
- `npm run build && npm run typecheck`;driver 的 ESLint 干净;
@@ -601,8 +613,8 @@ relay 那一个修补 Java 源码:
声明故障都点火 —— 每个丢弃计数、扣留计数、`fg9b_fire` 计数与被扣住的
kill 点都被观察到 —— 按字母选择子可用,且快乐路径(无注入的一次
广播)全绿、每个 roster 位置恰好一行 `msgc_`。
- 以上切片 1 与切片 2 变异每个单独变红;源码还原;门禁复绿。切片 3 对其
变异重复同一流程。
- 以上切片 1、切片 2 与残余轮的变异每个单独变红;源码还原;门禁复绿。
切片 3 对其变异重复同一流程。
- `hosted-process-ci.test.js` 带任何移动的预算全绿,且全车道跑完后
`check-failsafe-reports.js hosted` 满足。
- 按 AGENTS.md 做两轮连续干净的无方向审计与反向审计。
@@ -617,6 +629,7 @@ relay 那一个修补 Java 源码:
稳定断言由三个 store 边界字母承载。
2. 广播 roster 是否扩到四名成员(三个收件人)以让部分臂的判定更锐 ——
代价是每个用例多一名脚本化成员;实现时定。
3. 切片 2 已定:FG9e 不武装过期游标 `resync_required` 用例。resync 应答
属于 Session 流的重放地板,与事件由哪个能力产生无关,FG8 的跟踪 issue
(#14163)仍欠着那唯一的用例。
3. 残余轮(#15750)已关闭:FG9e 现在武装过期游标 `resync_required`
用例(`cursor-expired-resync`)。resync 应答属于 Session 流的重放地板,
与事件由哪个能力产生无关;FG8 的跟踪 issue(#14163)把该格子延后,
由 #15750 承接。
@@ -234,8 +234,15 @@ Mechanics notes recorded at landing:
windows, so the slice's mutations remove all of them at once (see
_Mutation checks_).
- The `CHILD_RESULT` ledger row is classified after the record's consumed
revision lands, so the gates wait for both.
- The `CHILD_RESULT` ledger row mirrors the record's delivery, and the
two writes can race: classification may commit after acceptance but
before the parent's consuming revision, so the gates wait for both.
The consuming revision then corrects that row's `consumption_state` in
its own commit — a mirror left `pending` would keep claiming the
parent never read a result its record marks consumed. The
classification's read locks that record row by its primary key, so a
consumption commit cannot slip between that read and the mirror write
and no sibling record in the Session is locked.
- The lead's wake Turn is not projected to its public stream (as FG9e
found for member wake Turns); the workflow child Session's Turn is,
from `turn.accepted` through `turn.completed`. FG10f therefore cuts the
@@ -200,8 +200,11 @@ store 客户端在一次提交调用内部重驱的丢弃,根本到不了 rela
在这些窗口上,单独中和其中任何一层都是等价变异,所以本片的变异一次去掉全部
防线(见「变异检查」)。
- `CHILD_RESULT` 台账行在记录的 consumed revision 落地之后才被归类,所以门禁两者
都要等。
- `CHILD_RESULT` 台账行镜像记录的投递状态,两处写入可能竞态:分类可能落在受理
之后、父级消费 revision 之前,所以门禁两者都要等。父级的消费 revision 随后在
自己的提交里修正该行的 `consumption_state` —— 否则镜像会一直停在 `pending`,
而记录早已标记结果被消费。分类读取记录行时按主键加锁,因此消费提交无法插进
这次读与镜像写入之间,同 Session 的其他记录也不会被锁住。
- lead 的唤醒 Turn 不投影到它的公开流(与 FG9e 对成员唤醒 Turn 的发现一致);
workflow 子 Session 的 Turn 会投影,从 `turn.accepted` 到 `turn.completed`。所以
FG10f 切断的是子 Session 的流,而不是 lead 的流:lead 的台账在启动 Turn 之后就
@@ -114,3 +114,39 @@ Every case in the class therefore uses a 30 s writer lease, including `harnessKi
Re-measured with the class-wide lease on JDK 21, MySQL 8.4.6 and Linux aarch64: the class passes 9/9 twice (104.4 s and 105.9 s). The R1-1 cell takes 10.8–11.4 s against its recorded 11.4–11.9 s, and every other case stays within its earlier range, so the budget entries above stand. Those runs predate the writer-scoped expiry; with it the class passes 9/9 again (104.8 s, and 107.8 s once it also checks that the leases lapsed), the Harness-kill cases at 11.9–13.4 s. The expiry is asserted rather than inferred from timings: without it each Harness-kill case would still pass, but only after waiting out the full 30 s lease (about 27 s longer per case in a review run).
`HostedMcpStoreFaultGatesIT` is outside this change and still exposed. Three of its cells run the same close preamble (`prepareClose()`: `admitClose`, then `effects` on a live Harness) at a 3 s writer lease, and it has not been measured. Its five Harness kills expire nothing, so a longer lease there has to come with the same dead-writer expiry, or each kill case waits out the lease. The residual is recorded on #15011.
## Follow-up cell: the first release refused with two servers
#15750 carries the FG12d cell that no class gated: the first `mcp-release` refused before forwarding when the Session has **two** servers. The existing `firstReleaseRefusedBeforeForwardingResumesUnderTheSameId` covers one server; this follow-up extends the same fixture rather than opening a new class.
| Case | Injection | Expect |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `first-release-refused-two-servers` | The session pins `fixture` revision 1 and `fixture2` revision 3; the worker MCP config adds `fixture2` revision 3 for this case only. After `prepare_resources`, the proxy refuses the next `mcp-release` before forwarding; the settle route is retried | The first configuration's release answers 503 and is retried under the same `${configurationId}:release` id; the second configuration then releases under its own id. Exactly three release controls (503, 200 same id, 200 other id), both configurations `released`, both fixture servers exit (`mcp.start` revisions 1 and 3, two `mcp.exit`), and no `mcp-configure`/`mcp-invoke` after the baseline. |
Decisions:
1. **One class, per-case config.** `HostedMcpRestartFaultGatesIT` adds `fixture2` only for this method's boot; `baseline(session, 2)` waits for both configurations to settle. No new class, no new profile, and the artifact root stays `target/fg12-cases/fg12d/`.
2. **Two pins, two configurations.** The session's `mcpServers` carries `mcpPin(1)` and `mcpPin("fixture2", 3)`; the Harness installs one configuration per server. Revision 3 distinguishes the second server's fixture events from `fixture`'s revision 1.
3. **The same-id retry is the claim.** The first 503 and the next 200 carry the same `${configurationId}:release` id; the third 200 carries the other configuration's id. The second configuration is reached only after the first settles.
Mutation witness: `drainConfigurations` iterates `configurations.slice(0, 1)` in a copy of the packaged CLI. The two-server case is red at `settle must complete` because the second configuration stays active; the one-server case stays green, so this case is the one that pins the second configuration. The restored bundle is green.
Budget: the two-server case measures 16.16–16.24 s standalone (two runs) on JDK 21, MySQL 8.4.6 and Linux x86_64; the ten-case class is 81.6 s. It adds 32 s (2×) to the shared healthy sum (11504 → 11536 s), so the `hosted-harness-mysql` fork moves to 11536 s and the Verify step to 203 minutes (12136 s of fork plus wrap); the job ceiling moves to 326 (293 + 33). The `hosted-mcp-gates` profile and the artifact root are unchanged.
## Follow-up cell: detach refused while a tool result is unknown
#15750's FG12d row also asks for a detach that refuses while a tool result is unknown. The class's existing unknown-result cases (`stdioKillDuringRequestSettlesOutcomeUnknownOnMySql` and `workerKillDuringOperationSettlesOutcomeUnknownOnMySql`) stop at the unknown; this follow-up attempts the Harness detach route while the record is `recovery_blocked` / `outcome_unknown`.
| Case | Injection | Expect |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `detach-refused-unknown-result` | Start an `ext://hold` raw operation through the private route; kill the stdio fixture; wait for `outcome_unknown` / `recovery_blocked`; POST the Harness detach route | Detach answers 409 `hosted_turn_active` or 503 `managed_session_close_failed` (the close catches the MCP recovery error); no `mcp-release` control; the operation stays unknown; the Session remains attached and reopenable. |
Decisions:
1. **The Harness detach route is the gate.** The control-plane lifecycle close is exercised by the R1-1 and two-server cells; this cell calls `POST /session/{id}/detach` directly, which is the production detach that reaches `HostedMcpSession.close()`.
2. **The unknown is permanent, not a timed window.** The stdio fixture is killed with an `ext://hold` read in flight, so the operation can only settle through a conclusive answer from its original Runtime generation. The detach has no window to race.
3. **The refusal is one of the documented close answers.** The test accepts 409 `hosted_turn_active` (MCP busy/recovering) and 503 `managed_session_close_failed` / `hosted_mcp_recovery_required`; it rejects 200/204 and asserts no release control and an unchanged unknown record.
Mutation witness: wrap `session.mcp?.close()` in the close path of a packaged CLI copy in `try { … } catch {}`; detach answers 204 and the case is red at the status assertion. The restored bundle is green.
Budget: the case measures 27.4 s worst of two runs (27.4 s and 17.4 s) on JDK 21, MySQL 8.4.6 and Linux x86_64; booked at 55 s (2×). The `hosted-harness-mysql` fork moves to 11591 s, the Verify step to 204 minutes and the job ceiling to 327 (294 + 33).
@@ -114,3 +114,39 @@ v2 关闭被准入之后,store 会 fence 普通写者续租,而 Harness 要
使用全类统一租约后,在 JDK 21、MySQL 8.4.6、Linux aarch64 上复测:整类两次 9/9 通过(104.4 s 与 105.9 s)。R1-1 格耗时 10.8–11.4 s,此前记录为 11.4–11.9 s;其余用例都在原有区间内,因此上文的预算条目仍然成立。这两次运行早于按写者限定的置过期;改为按写者限定后,整类再次 9/9 通过(104.8 s;再加上租约已失效的检查后为 107.8 s),杀 Harness 的用例耗时 11.9–13.4 s。置过期由断言钉住,而不是靠耗时推断:没有它,每个杀 Harness 的用例仍会通过,但要先等满 30 s 租约(评审运行中每个用例约慢 27 s)。
`HostedMcpStoreFaultGatesIT` 不在这次改动范围内,仍暴露在这个窗口下:它有三个格在 3 s 写者租约下跑同样的关闭前置(`prepareClose()`:先 `admitClose`,再在存活的 Harness 上执行 `effects`),且没有测量过。它的五处杀 Harness 都不做置过期,所以那里若改用更长的租约,必须同时带上同样的死写者置过期,否则每个杀 Harness 的用例都要等满租约。这一残留记录在 #15011。
## 后续格:两个 server 时首次 release 被拒
#15750 承接的 FG12d 格:Session 配置两个 server 时,第一次 `mcp-release` 在转发前被拒。现有 `firstReleaseRefusedBeforeForwardingResumesUnderTheSameId` 只覆盖一个 server;本后续格复用同一夹具,不新开类。
| 用例 | 注入 | 期望 |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `first-release-refused-two-servers` | Session pin `fixture` revision 1 与 `fixture2` revision 3;worker MCP 配置只为该用例加入 `fixture2` revision 3。`prepare_resources` 之后,proxy 在转发前拒绝下一次 `mcp-release`;settle 路由重试 | 第一个配置的 release 返回 503,并以同一个 `${configurationId}:release` id 重试;随后第二个配置以自己的 id 释放。恰好三个 release 控制(503、同 id 200、另一个 id 200),两个配置都 `released`,两个 fixture server 都退出(`mcp.start` revision 1 与 3,两次 `mcp.exit`),baseline 之后没有 `mcp-configure`/`mcp-invoke`。 |
决策:
1. **同一类,按用例配置。** `HostedMcpRestartFaultGatesIT` 只在该方法的 boot 中加入 `fixture2`;`baseline(session, 2)` 等待两个配置 settled。不新开类、不新增 profile,产物根仍是 `target/fg12-cases/fg12d/`。
2. **两个 pin,两个配置。** Session 的 `mcpServers` 带 `mcpPin(1)` 与 `mcpPin("fixture2", 3)`;Harness 为每个 server 安装一个配置。revision 3 把第二个 server 的 fixture 事件与 `fixture` 的 revision 1 区分开。
3. **同 id 重试是承重断言。** 第一个 503 与随后的 200 带同一个 `${configurationId}:release` id;第三个 200 带另一个配置的 id。只有第一个配置 settled 之后才会到达第二个配置。
变异见证:在打包 CLI 的副本中让 `drainConfigurations` 只遍历 `configurations.slice(0, 1)`。两个 server 的用例在 `settle must complete` 处变红,因为第二个配置保持 active;一个 server 的用例仍绿,因此是这个用例钉住了第二个配置。还原后的 bundle 复绿。
预算:两个 server 的用例在 JDK 21、MySQL 8.4.6、Linux x86_64 上单独实测 16.16–16.24 秒(两次),十用例整类 81.6 秒。按 2× 记 32 秒加入共享健康总和(11504 → 11536 秒),`hosted-harness-mysql` fork 移到 11536 秒,Verify 步骤移到 203 分钟(含 wrap 共 12136 秒),job 上限移到 326(293 + 33)。`hosted-mcp-gates` profile 与产物根不变。
## 后续格:工具结果未知时 detach 被拒
#15750 的 FG12d 行还要求:工具结果未知时 detach 必须被拒。本类已有的 unknown 结果用例(`stdioKillDuringRequestSettlesOutcomeUnknownOnMySql` 与 `workerKillDuringOperationSettlesOutcomeUnknownOnMySql`)只停在 unknown;本后续格在记录处于 `recovery_blocked` / `outcome_unknown` 时调用 Harness 的 detach 路由。
| 用例 | 注入 | 期望 |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `detach-refused-unknown-result` | 通过私有路由发起 `ext://hold` 原始操作;杀掉 stdio fixture;等 `outcome_unknown` / `recovery_blocked`;POST Harness detach 路由 | detach 返回 409 `hosted_turn_active` 或 503 `managed_session_close_failed`(close 捕获 MCP recovery 错误);没有 `mcp-release` 控制;操作保持 unknown;Session 保持 attached、可重开 |
决策:
1. **以 Harness detach 路由为门禁。** 控制面 lifecycle close 已由 R1-1 与两 server 用例覆盖;本格直接调 `POST /session/{id}/detach`,即生产 detach 到达 `HostedMcpSession.close()` 的路径。
2. **unknown 是永久的,不是定时窗口。** 带 `ext://hold` 读在飞时杀掉 stdio fixture,操作只能由原 Runtime 代的确定性应答结算;detach 没有可赛跑的窗口。
3. **拒绝码取自已记录的关闭应答。** 测试接受 409 `hosted_turn_active`(MCP busy/recovering)与 503 `managed_session_close_failed` / `hosted_mcp_recovery_required`;拒绝 200/204,并断言没有 release 控制、记录仍是 unknown。
变异见证:在打包 CLI 副本的 close 路径中把 `session.mcp?.close()` 包进 `try { … } catch {}`;detach 返回 204,用例在状态断言处变红。还原后的 bundle 复绿。
预算:该用例在 JDK 21、MySQL 8.4.6、Linux x86_64 上两次实测取最慢 27.4 秒(27.4/17.4),按 2× 记 55 秒。`hosted-harness-mysql` fork 移到 11591 秒,Verify 移到 204 分钟,job 上限移到 327(294 + 33)。
+14 -4
View File
@@ -1832,10 +1832,20 @@ Validation:
rollup paging; the reviewer probe's events, filters and cursor pruning.
- Push guard tests against a local bare repository: non-fast-forward refusal,
default-branch refusal, lease rejection after an external push.
- An integration test with a fake `gh` on `PATH` that drives `prepare` →
`snapshot` → `push` → `reply` → `cleanup` — not written yet; each helper
is tested on its own against real git with a bare remote and a stubbed
`gh`.
- An integration test with a fake `gh` on `PATH`
(`autofix-pr/chain.integration.test.ts`) that runs the helpers in the
order a watch runs them — `prepare` and `schedule`; a round's `sync`,
`snapshot`, `ci-log`, `rerun`, `push` and `reply`; the gate exactly as
`schedule` printed it; `status` and `cleanup` — against real git with a
bare remote, the test making the fix agent's commits. Its cases: a watch
from start to merge, including a reply whose answer was lost; a push by
someone else during a round; a re-run spent, then refused; a job that
timed out re-run once, a re-run whose answer was lost counted; a check
left for a person after three pushes, one head's result asked of GitHub
by commit; a conflict left for a person once per head, a head the watch
pushed before GitHub computed it included; and whether the PR's author
can push, asked of GitHub. Each helper is also tested on its own, with
`gh` stubbed in-process.
- Manual end-to-end on a sandbox repository: a failing test is fixed and
pushed; a collaborator comment is fixed and answered; a stranger's comment is
ignored; merging the PR ends the loop and removes the worktree.
@@ -555,7 +555,7 @@ resync 时如果新 head 不包含 worktree 的 `HEAD`(作者 rebase 或 force
- 用录制的 `gh` JSON fixture 做单元测试:事件对比、旧 SHA 上的过时检查、各信任类别、回复标记和 id 的自我排除、动作优先级、预算;闸门游标规则(每个 head 一次、重跑后的失败、冲突标记、当前用户自己的评论、合并与重新打开)和 check 汇总翻页;评审者探针的事件、过滤项和游标清理。
- 用本地 bare 仓库测试推送保护:拒绝非 fast-forward、拒绝默认分支、别人推送后 lease 被拒。
- 在 `PATH` 上放一个假的 `gh` 做集成测试,跑通 `prepare` → `snapshot` → `push` → `reply` → `cleanup`——尚未编写;目前每个辅助命令各自用真实 git、裸远端仓库和打桩的 `gh` 测试。
- 在 `PATH` 上放一个假的 `gh` 做集成测试(`autofix-pr/chain.integration.test.ts`):按监视运行它们的顺序依次运行各辅助命令——`prepare` 和 `schedule`;一轮里的 `sync`、`snapshot`、`ci-log`、`rerun`、`push` 和 `reply`;按 `schedule` 打印的原样执行的 gate;`status` 和 `cleanup`——用真实 git 和裸远端仓库,修复 agent 的提交由测试代为完成。用例:从启动到合入的一次监视,其中有一次回复的应答丢失;一轮进行中别人推送;重跑用掉一次后被拒;超时的 job 只重跑一次,应答丢失的那次重跑也计数;三次推送后仍失败的检查交给人,其中一个 head 的结果按提交向 GitHub 查询;冲突每个 head 只交给人一次,监视自己推送、GitHub 还没算出结果的 head 也算在内;向 GitHub 查询 PR 作者能否推送。每个辅助命令另有各自的测试,`gh` 在进程内打桩。
- 在沙箱仓库上手工端到端验证:失败的测试被修复并推送;协作者的评论被修复并回复;陌生人的评论被忽略;合并 PR 后循环结束,worktree 被删除。
验收标准:
@@ -110,10 +110,13 @@ under `stop` it says that continuing will stop the turn.
`loop_detected` and the `StopFailure` hook keep meaning "the turn stopped". A
new `loop_protection` event records each governed decision: `reminded`,
`stopped`, or `observed` under `off`, with the loop type, the reason and the
mode. `reminded` is logged when the reminder is sent, so a reminder dropped by
a retry or cut off by a stop in the same batch is not counted; `stopped` is
logged once per turn. Budget stops and the provider-id breaker are not
governed and emit no decision event.
mode. `reminded` is logged when the model accepts the request carrying the
reminder, so a reminder dropped by a retry, cut off by a stop in the same
batch, or carried only by a send that never reached the model is not
counted — in that last case the warning stays eligible and is credited by
the send that does reach the model; `stopped` is logged once per turn.
Budget stops and the provider-id breaker are not governed and emit no
decision event.
### Messages
@@ -70,7 +70,7 @@
### 遥测
`loop_detected` 和 `StopFailure` hook 仍然表示“turn 已停止”。新增的 `loop_protection` 事件记录每个受管控的决策:`reminded`、`stopped`,或 `off` 下的 `observed`,并带上循环类型、原因和模式。`reminded` 在提醒发出时记录,所以被重试丢弃、或在同一批中被停止截断的提醒不计入;`stopped` 每个 turn 只记录一次。预算停止和 provider id 熔断器不受管控,不产生决策事件。
`loop_detected` 和 `StopFailure` hook 仍然表示“turn 已停止”。新增的 `loop_protection` 事件记录每个受管控的决策:`reminded`、`stopped`,或 `off` 下的 `observed`,并带上循环类型、原因和模式。`reminded` 在模型接受承载该提醒的请求时记录,所以被重试丢弃、在同一批中被停止截断、或只由未真正送达模型的发送承载的提醒都不计入——最后一种情况下该提醒保持可用,由真正送达模型的那次发送计入;`stopped` 每个 turn 只记录一次。预算停止和 provider id 熔断器不受管控,不产生决策事件。
### 提示文案
@@ -23,7 +23,7 @@ The first delivery uses one Harness endpoint and operator-managed model credenti
| Harness routing | Java configures one Harness base URL and one client. Sequential process replacement exists; concurrent replica selection and durable Session routing require further work. |
| Runtime isolation | Broker scope includes tenant and Workspace, with Workspace or Session isolation classes. These logical scopes do not by themselves establish OS-level isolation between untrusted users. |
Source references: [identity filter](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/api/TenantContextFilter.java), [public controller](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/api/PublicAgentController.java), [Hosted Sessions](../../packages/cli/src/serve/hosted-harness-session.ts), [Hosted model](../../packages/cli/src/serve/hosted-harness-model.ts), [writer credentials](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/security/WriterCredentialPolicy.java), [connector](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/harness/QwenHostedHarnessConnector.java), and [Runtime scope](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/runtimebroker/RuntimeScope.java).
Source references: [identity filter](../../packages/managed-agent-server/server/src/main/java/com/alibaba/qwen/code/managedagent/api/TenantContextFilter.java), [public controller](../../packages/managed-agent-server/server/src/main/java/com/alibaba/qwen/code/managedagent/api/PublicAgentController.java), [Hosted Sessions](../../packages/cli/src/serve/hosted-harness-session.ts), [Hosted model](../../packages/cli/src/serve/hosted-harness-model.ts), [writer credentials](../../packages/managed-agent-server/contracts/src/main/java/com/alibaba/qwen/code/managedagent/contracts/security/WriterCredentialPolicy.java), [connector](../../packages/managed-agent-server/adapters/src/main/java/com/alibaba/qwen/code/managedagent/harness/QwenHostedHarnessConnector.java), and [Runtime scope](../../packages/managed-agent-server/contracts/src/main/java/com/alibaba/qwen/code/runtimebroker/RuntimeScope.java).
## Deployment and trust boundary
@@ -103,7 +103,7 @@ Harness placement and Tool Runtime provisioning are independent choices. Keep `Q
| `local-process` Tool Runtime | Keep the existing Broker selection and `LocalProcessRuntimeProvisioner` launch, ready handshake, execution identity, cancellation and recovery contracts. Harness horizontal scaling does not require replacing this provisioner or installing Kubernetes locally. |
| Kubernetes Harness with a local-process Runtime | Validate network reachability and storage ownership for the chosen topology. Harness must reach Java's Store and Broker addresses. A worker's loopback endpoint can remain local when consumed by its same-host Broker; callbacks and resource URLs need validation in their actual caller's network namespace. This mixed topology needs its own acceptance evidence. |
Reference: [local Runtime provisioner](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/runtimebroker/provisioning/LocalProcessRuntimeProvisioner.java).
Reference: [local Runtime provisioner](../../packages/managed-agent-server/runtime-local/src/main/java/com/alibaba/qwen/code/runtimebroker/provisioning/local/LocalProcessRuntimeProvisioner.java).
The shared multi-tenant deployment requires its stricter actor, writer-credential and capacity prerequisites. Preserve the existing explicitly trusted single-user local deployment's authentication and trust contract; locality must not silently exempt a deployment that actually serves multiple tenants. Any optional discovery and pool-lifecycle behavior stays outside the default single-address path. Source compatibility is a design requirement, not evidence that future common-code changes have already passed local regression or that local processes provide an untrusted-user sandbox.
@@ -23,7 +23,7 @@
| Harness 路由 | Java 配置一个 Harness 地址和一个客户端。已有进程顺序替换能力;并发副本选择和持久 Session 路由仍需补齐。 |
| Runtime 隔离 | Broker 范围包含租户和 Workspace,并区分 Workspace 或 Session 隔离类别。这些逻辑范围本身不能证明不可信用户之间的操作系统隔离。 |
源码参考:[身份过滤器](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/api/TenantContextFilter.java)、[公共 Controller](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/api/PublicAgentController.java)、[Hosted Session](../../packages/cli/src/serve/hosted-harness-session.ts)、[Hosted 模型](../../packages/cli/src/serve/hosted-harness-model.ts)、[writer 凭据](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/security/WriterCredentialPolicy.java)、[连接器](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/managedagent/harness/QwenHostedHarnessConnector.java)和 [Runtime 范围](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/runtimebroker/RuntimeScope.java)。
源码参考:[身份过滤器](../../packages/managed-agent-server/server/src/main/java/com/alibaba/qwen/code/managedagent/api/TenantContextFilter.java)、[公共 Controller](../../packages/managed-agent-server/server/src/main/java/com/alibaba/qwen/code/managedagent/api/PublicAgentController.java)、[Hosted Session](../../packages/cli/src/serve/hosted-harness-session.ts)、[Hosted 模型](../../packages/cli/src/serve/hosted-harness-model.ts)、[writer 凭据](../../packages/managed-agent-server/contracts/src/main/java/com/alibaba/qwen/code/managedagent/contracts/security/WriterCredentialPolicy.java)、[连接器](../../packages/managed-agent-server/adapters/src/main/java/com/alibaba/qwen/code/managedagent/harness/QwenHostedHarnessConnector.java)和 [Runtime 范围](../../packages/managed-agent-server/contracts/src/main/java/com/alibaba/qwen/code/runtimebroker/RuntimeScope.java)。
## 部署与信任边界
@@ -103,7 +103,7 @@ Harness 部署位置与 Tool Runtime 的创建方式是独立选择。保留 `QW
| `local-process` Tool Runtime | 保留已有 Broker 选择和 `LocalProcessRuntimeProvisioner` 的启动、ready 握手、执行身份、取消与恢复契约。Harness 水平扩展不要求替换该 provisioner,也不要求本地安装 Kubernetes。 |
| Kubernetes Harness 配合本地进程 Runtime | 针对所选拓扑验证网络可达性和存储归属。Harness 必须能访问 Java 的 Store 和 Broker 地址。同机 Broker 消费的 worker 回环端点可以继续本地使用;回调和资源 URL 必须按实际调用方的网络命名空间验证。此混合拓扑需要独立验收证据。 |
参考:[本地 Runtime provisioner](../../packages/managed-agent-server/src/main/java/com/alibaba/qwen/code/runtimebroker/provisioning/LocalProcessRuntimeProvisioner.java)。
参考:[本地 Runtime provisioner](../../packages/managed-agent-server/runtime-local/src/main/java/com/alibaba/qwen/code/runtimebroker/provisioning/local/LocalProcessRuntimeProvisioner.java)。
共享多租户部署要求更严格的 Actor、writer 凭据和容量前提。保留已有明确受信任的单用户本地部署的认证与信任契约;不能仅因部署在本地,就悄然豁免实际服务多租户的部署。可选的发现和服务池生命周期行为不进入默认单地址路径。源码兼容是设计要求,不代表未来共用代码变更已经通过本地回归,也不代表本地进程提供了不可信用户沙箱。
+33 -7
View File
@@ -176,13 +176,13 @@ A policy affects only the surface that passes it.
Some surfaces did part of the interaction step themselves before they moved, and keep doing it on the shared path for a while. A send option then tells the client not to do that part, so it does not run twice. Each such option is a transition step, not a lasting API: it is listed here with the step that removes it or turns it into an input.
| Option | Why the host keeps the part | Removed by |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toolLoopGuardsByHost` | ACP's tool step runs the daemon tool-loop guards: the per-turn tool-call cap, repeated calls, the exploration reminder and the repeated-tool-failure guard | PR 2b for a fresh prompt's tool batches: the client's guards apply, and ACP keeps the ones only it has, its parameter-error, partial-edit and repeated-tool-failure stops, as its policy. A session with live tools and an Agent Host session keep ACP's tool step and this option until PR 2d |
| `interactionSpanByHost` | ACP's span is parented to the daemon's trace | an input instead: the client joins the span the host started for the prompt; before the default changes for ACP |
| `userTurnRecordedByHost` | ACP records the prompt as typed, with its display text, attachments, resource links and daemon prompt id, before slash commands run, and takes the turn's file-history snapshot before its first send, so a prompt cancelled before it is sent still has one | an input instead: the host passes that record and the client writes it; before the default changes for ACP |
| Option | Why the host keeps the part | Removed by |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toolLoopGuardsByHost` | ACP's tool step runs the daemon tool-loop guards: the per-turn tool-call cap, repeated calls, the exploration reminder and the repeated-tool-failure guard | PR 2b for a fresh prompt's tool batches: the client's guards apply, and ACP keeps the ones only it has, its parameter-error, partial-edit and repeated-tool-failure stops, as its policy. A session with live tools and an Agent Host session keep ACP's tool step and this option until PR 2d |
| `interactionSpanByHost` | ACP's span is parented to the daemon's trace | an input instead: the client joins the span the host started for the prompt; before the default changes for ACP |
| `userTurnRecordedByHost` | ACP records the prompt as typed, with its display text, attachments, resource links and daemon prompt id, before slash commands run, and takes the turn's file-history snapshot before its first send, so a prompt cancelled before it is sent still has one; on a retry, a continue or a restore it has started the todo work chain | an input instead: the host passes that record and the client writes it; before the default changes for ACP |
`toolCallPreparations`, `memoryRecallQuery` and `stopPolicies` are not in the table: the first asks for an extra event, the second gives recall the prompt's text, which the host expanded before the send, and the third adds the host's reasons to continue at a stop point ([Stop-point continuations](#stop-point-continuations)); none turns anything off. PR 2c-1 removed `stopHookRunByHost`, with which ACP fired the Stop hook in its own Stop loop.
`toolCallPreparations`, `memoryRecallQuery`, `stopPolicies` and `historyPromptId` are not in the table: the first asks for an extra event, the second gives recall the prompt's text, which the host expanded before the send, the third adds the host's reasons to continue at a stop point ([Stop-point continuations](#stop-point-continuations)), and the fourth gives a `Retry` the history identity of the attempt it replaces; none turns anything off. PR 2c-1 removed `stopHookRunByHost`, with which ACP fired the Stop hook in its own Stop loop.
A new option that turns off part of the client's work for one host needs a row here, and a surface's default does not change while it has an open row.
@@ -241,7 +241,7 @@ Once surfaces change their defaults one at a time, the setting has no schema def
### ACP prompt turns on the shared round (PR 2a)
PR 2 moves ACP in steps. PR 2a moves the model round of a fresh user prompt: one that is not a retry, a continue or a restore, and not a Goal, channel or goal-proposal approval turn. The round is the loop's own `runModelRound`: one `LlmClient.sendMessageStream` call whose events go to ACP and whose terminal events become stop reasons. ACP keeps its tool step (`runToolCalls`, the next message after tools, the daemon tool-loop guards) and everything around the turn. PR 2b moves the tool batch of these turns onto the scheduler, and PR 2c-1 their stop point onto the client's chain. PRs 2c-2 to 2c-4 then move the other turns, so each of ACP's loop bodies changes once.
PR 2 moves ACP in steps. PR 2a moves the model round of a fresh user prompt: one that is not a retry, a continue or a restore, and not a Goal, channel or goal-proposal approval turn (PR 2c-2 moves all of them but Goal turns, which PR 2c-3 moves). The round is the loop's own `runModelRound`: one `LlmClient.sendMessageStream` call whose events go to ACP and whose terminal events become stop reasons. ACP keeps its tool step (`runToolCalls`, the next message after tools, the daemon tool-loop guards) and everything around the turn. PR 2b moves the tool batch of these turns onto the scheduler, and PR 2c-1 their stop point onto the client's chain. PRs 2c-2 to 2c-4 then move the other turns, so each of ACP's loop bodies changes once.
| ACP today | With the setting on |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -314,6 +314,26 @@ Hook diagnostics do not reopen a finished response's interruption window. Each n
The core change of this step adds `SendMessageOptions.stopPolicies` with the `StopPolicy` interface and removes `stopHookRunByHost`, which only ACP passed.
### ACP turns that re-enter a prompt, and channel and Goal-proposal turns (PR 2c-2)
PR 2c-2 puts five more kinds of ACP turn on the shared round, the shared tool batch and the client's stop point. Two are fresh prompts PR 2a left out: channel prompts and Goal-proposal approval turns. Three re-enter a turn the session already has: a retry, a continue of an interrupted turn and the restore of an unanswered `ask_user_question`. Goal turns, cron and background notifications move in PRs 2c-3 and 2c-4.
| Turn | With the setting on |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| channel prompt | a `UserQuery` like any fresh prompt, without the submitted prompt or workflow input, as today; its responses are not cut short by queued input, its tool results never end the turn, and its repeated-tool-failure guard stays off |
| Goal-proposal approval | a `UserQuery`; the session settles the proposal as today, and a Todo Stop Guard that gives way, or the Stop hook's cap, keeps the proposal from starting a Goal |
| retry | ACP strips the failed attempt and puts it back if the retry never sends, as today; the first round is a `Retry` with `historyPromptId`, the attempt's identity, and UserPromptSubmit's verdict from `admitUserPrompt` |
| continue | ACP strips the interrupted run, or closes the interrupted calls, as today; the first round is a `Retry` with `historyPromptId` and no UserPromptSubmit; a first request that never goes out puts the interrupted run back as it was |
| restore of `ask_user_question` | ACP asks the question again with its own tool step; the answer is the first round, a `Retry`; later batches run on the shared batch |
A re-entered turn's first round is a `Retry`, not a `ToolResult`, even when it carries tool results: a `ToolResult` does not start the client's interaction, so the previous prompt's loop-detector counts would carry over and UserPromptSubmit would run on the results. Headless continues an interrupted turn with a `ToolResult` because each of its runs is a new process. A `Retry` gets no system reminders from the client, so ACP adds its plan, arena and output-style reminders to it as today, and the todo reminder ACP takes is the one the client would take.
The restored question keeps ACP's tool step: the shared batch records every result, and a question left unanswered on a timeout must stay unrecorded so a later load can ask it again.
Whether a re-entered turn's first request went out is read from the chat's push count when the round ends, on every exit. A request in history replaces what the turn stripped and carries the restore's notices, even when its response then fails; a request that never went out leaves history as the turn found it, and the notices stay pending.
The core changes of this step: `userTurnRecordedByHost` also covers a `Retry`, whose todo work chain the host started; `promptAdmission` is accepted on a `Retry`; and the new input `historyPromptId`, the history identity a `Retry`'s request carries, spent by its first request.
## Migration
| PR | Content |
@@ -384,6 +404,9 @@ A difference fails the scenario unless it is listed with a linked issue.
- an edit cut off by the output limit;
- a stream that drops and is retried;
- two prompts in one session;
- a channel prompt that reads a file, from a client that holds the private parent capability so the agent keeps the channel mark;
- a retry after a model error;
- a retry that fails again after its request reached history;
- a model error;
- a UserPromptSubmit hook that blocks;
- a Stop hook that blocks once, and one that blocks up to the cap;
@@ -426,6 +449,9 @@ The suite runs in the integration job. A scheduled job also runs the whole headl
- from PR 2c-1, the current turn's background results that arrive after its last response start a background-notification turn instead of continuing the prompt, and with the Todo Stop Guard armed, input queued mid-turn becomes the next prompt instead of continuing this one;
- the Todo Stop Guard continues only once the Stop hook lets the response stop, in a request of its own instead of one merged with the hook's reason, and input that arrives while the Stop hook runs no longer discards its decision;
- Stop-hook and Todo Stop Guard continuations run under the turn's prompt id, run UserPromptSubmit as the TUI's do, run the client's tool-call guards except in Live and Agent Host sessions, which keep ACP's, and send their tool results back as any batch does;
- from PR 2c-2, a channel prompt and a Goal-proposal turn get what a fresh prompt gets on the shared path, the date and system reminders and the client's recall included;
- a retry, a continue and a restore start the client's interaction: its loop detector, exploration budget and turn budget start over, and their first round does not count toward the session turn cap;
- a continue whose first request never goes out, cancelled or failed, puts the interrupted user run back as it was, instead of keeping it as one merged entry, and the worktree notice the merged entry took stays pending for the next turn;
The ACP parity suite covers the common cases unchanged and asserts the date reminder, UserPromptSubmit on tool results, cancelling while that hook runs, and that a batch's approvals come before its calls run.
@@ -176,13 +176,13 @@ headless 的主循环以 `maxRounds: 1` 运行。因此它为每一轮做的准
有些入口在迁移前自己做了交互步骤中的一部分工作,迁到共享路径后还会继续自己做一段时间。这时用一个发送选项告诉 client 跳过这部分,避免执行两次。这类选项只是迁移期的过渡手段,不是长期 API。下表逐项列出,并写明由哪一步把它删除或改成输入。
| 选项 | 宿主为什么保留这部分 | 由哪一步移除 |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toolLoopGuardsByHost` | ACP 的工具步骤运行 daemon 工具循环守卫:每轮工具调用上限、重复调用、探索提醒,以及重复工具失败守卫 | PR 2b 迁移全新 prompt 的工具批:client 的守卫生效,只有 ACP 才有的守卫,即它的参数错误、半截编辑和重复工具失败停止,作为 ACP 的策略保留。带 live 工具的会话和 Agent Host 会话保留 ACP 的工具步骤和这个选项,直到 PR 2d |
| `interactionSpanByHost` | ACP 的 span 挂在 daemon 的 trace 下 | 改为输入:client 加入宿主为该 prompt 开启的 span;ACP 修改默认值之前完成 |
| `userTurnRecordedByHost` | ACP 在 slash 命令执行前,按用户输入的原样记录 prompt,连同显示文本、附件、resource link 和 daemon prompt id 一起记录,并在首次发送前为这一轮建立文件历史快照,所以发出前就被取消的 prompt 也有快照 | 改为输入:宿主传入这条记录,由 client 写入;ACP 修改默认值之前完成 |
| 选项 | 宿主为什么保留这部分 | 由哪一步移除 |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toolLoopGuardsByHost` | ACP 的工具步骤运行 daemon 工具循环守卫:每轮工具调用上限、重复调用、探索提醒,以及重复工具失败守卫 | PR 2b 迁移全新 prompt 的工具批:client 的守卫生效,只有 ACP 才有的守卫,即它的参数错误、半截编辑和重复工具失败停止,作为 ACP 的策略保留。带 live 工具的会话和 Agent Host 会话保留 ACP 的工具步骤和这个选项,直到 PR 2d |
| `interactionSpanByHost` | ACP 的 span 挂在 daemon 的 trace 下 | 改为输入:client 加入宿主为该 prompt 开启的 span;ACP 修改默认值之前完成 |
| `userTurnRecordedByHost` | ACP 在 slash 命令执行前,按用户输入的原样记录 prompt,连同显示文本、附件、resource link 和 daemon prompt id 一起记录,并在首次发送前为这一轮建立文件历史快照,所以发出前就被取消的 prompt 也有快照;在 retry、continue 或 restore 上,宿主已经启动了 todo 工作链 | 改为输入:宿主传入这条记录,由 client 写入;ACP 修改默认值之前完成 |
`toolCallPreparations`、`memoryRecallQuery` 和 `stopPolicies` 不在表中:第一个只是请求一个额外的事件,第二个把宿主在发送前展开的 prompt 原文交给召回,第三个加入宿主在 stop 点继续的理由(见 [stop 点续发](#stop-点续发));三者都不关闭任何行为。PR 2c-1 删除了 `stopHookRunByHost`,ACP 原来用它在自己的 Stop 循环里触发 Stop hook。
`toolCallPreparations`、`memoryRecallQuery`、`stopPolicies` 和 `historyPromptId` 不在表中:第一个只是请求一个额外的事件,第二个把宿主在发送前展开的 prompt 原文交给召回,第三个加入宿主在 stop 点继续的理由(见 [stop 点续发](#stop-点续发)),第四个让 `Retry` 沿用它所替代那次尝试的历史标识;四者都不关闭任何行为。PR 2c-1 删除了 `stopHookRunByHost`,ACP 原来用它在自己的 Stop 循环里触发 Stop hook。
新增任何为某个宿主关闭 client 部分工作的选项,都必须在这里登记一行。入口在表中还有未关闭的行时,不修改它的默认值。
@@ -241,7 +241,7 @@ if (config.isUnifiedAgentLoopEnabled()) {
### ACP prompt 轮次迁到共享模型轮(PR 2a)
PR 2 分步迁移 ACP。PR 2a 迁移全新用户 prompt 的模型轮:不是 retry、continue 或 restore,也不是 Goal、channel 或 Goal 提案审批轮次的 prompt。模型轮就是共享 loop 自己的 `runModelRound`:一次 `LlmClient.sendMessageStream` 调用,事件交给 ACP,终止事件变成停止原因。ACP 保留自己的工具步骤(`runToolCalls`、工具之后的下一条消息、daemon 工具循环守卫)以及轮次前后的全部逻辑。PR 2b 把这些轮次的工具批迁到 scheduler 上,PR 2c-1 把它们的 stop 点迁到 client 的链上。PR 2c-2 到 2c-4 再迁移其他轮次,因此 ACP 的每份 loop 主体只改动一次。
PR 2 分步迁移 ACP。PR 2a 迁移全新用户 prompt 的模型轮:不是 retry、continue 或 restore,也不是 Goal、channel 或 Goal 提案审批轮次的 prompt(除 Goal 轮次由 PR 2c-3 迁移外,其余都由 PR 2c-2 迁移)。模型轮就是共享 loop 自己的 `runModelRound`:一次 `LlmClient.sendMessageStream` 调用,事件交给 ACP,终止事件变成停止原因。ACP 保留自己的工具步骤(`runToolCalls`、工具之后的下一条消息、daemon 工具循环守卫)以及轮次前后的全部逻辑。PR 2b 把这些轮次的工具批迁到 scheduler 上,PR 2c-1 把它们的 stop 点迁到 client 的链上。PR 2c-2 到 2c-4 再迁移其他轮次,因此 ACP 的每份 loop 主体只改动一次。
| ACP 现在 | 设置开启后 |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -314,6 +314,26 @@ hook 诊断消息不会重新打开已结束回复的中断窗口。每个新的
这一步对 core 的改动是新增 `SendMessageOptions.stopPolicies` 和 `StopPolicy` 接口,并删除只有 ACP 使用的 `stopHookRunByHost`。
### 重新进入轮次的 ACP prompt,以及 channel 和 Goal 提案轮次(PR 2c-2)
PR 2c-2 让另外五类 ACP 轮次使用共享模型轮、共享工具批和 client 的 stop 点。其中两类是 PR 2a 没有纳入的全新 prompt:channel prompt 和 Goal 提案审批轮次。另外三类重新进入会话中已有的轮次:retry、继续被打断的轮次(continue),以及恢复一个未回答的 `ask_user_question`(restore)。Goal 轮次、cron 和后台通知在 PR 2c-3 和 2c-4 中迁移。
| 轮次 | 设置开启后 |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| channel prompt | 与其他全新 prompt 一样是 `UserQuery`,和现在一样不带 submitted prompt 和 workflow 输入;它的回复不会被排队的输入打断,它的工具结果从不结束这一轮,重复工具失败守卫保持关闭 |
| Goal 提案审批 | `UserQuery`;会话和现在一样结算提案;Todo Stop Guard 让位或 Stop hook 达到上限时,提案不会启动 Goal |
| retry | 和现在一样由 ACP 剥离失败的那次尝试,retry 一直没有发出时再把它放回;第一轮是 `Retry`,带上那次尝试的标识 `historyPromptId`,以及 `admitUserPrompt` 给出的 UserPromptSubmit 结果 |
| continue | 和现在一样由 ACP 剥离被打断的那段输入,或关闭被打断的调用;第一轮是 `Retry`,带 `historyPromptId`,不运行 UserPromptSubmit;第一条请求一直没有发出时,被打断的那段输入按原样放回 |
| 恢复 `ask_user_question` | ACP 用自己的工具步骤重新提问;回答作为第一轮,即一个 `Retry`;之后的批次使用共享工具批 |
重新进入的轮次即使带着工具结果,第一轮也是 `Retry` 而不是 `ToolResult`:`ToolResult` 不会开启 client 的一次交互,上一个 prompt 的循环检测计数会延续下来,UserPromptSubmit 也会在这些结果上运行。headless 用 `ToolResult` 继续被打断的轮次,是因为它每次运行都是一个新进程。client 不给 `Retry` 加系统提醒,所以 ACP 和现在一样为它加上 plan、arena 和 output-style 提醒;ACP 取走的 todo 提醒就是 client 会取的那一条。
被恢复的问题保留 ACP 的工具步骤:共享工具批会记录每一个结果,而因超时没有回答的问题必须保持未记录,之后加载时才能再次提问。
重新进入的轮次的第一条请求是否已经发出,在模型轮结束时按 chat 的推送计数判断,每条退出路径都一样。已进入历史的请求替换轮次剥离的内容,并带上 restore 的提醒,即使随后响应失败也是如此;一直没有发出的请求让历史保持轮次开始时的样子,提醒保持待发。
这一步对 core 的改动:`userTurnRecordedByHost` 也覆盖 `Retry`,表示宿主已经启动了它的 todo 工作链;`Retry` 也接受 `promptAdmission`;新增输入 `historyPromptId`,即 `Retry` 的请求在历史中带的标识,由第一条请求使用。
## 迁移计划
| PR | 内容 |
@@ -384,6 +404,9 @@ PR 3–6 每一项之后,同样各自跟随默认开启和删除旧 loop 两
- 被输出上限截断的编辑;
- 中途断流后重试的流;
- 同一会话中的两个 prompt;
- 读取文件的 channel prompt,由持有私有父能力的客户端发出,因此 agent 保留 channel 标记;
- 模型报错之后的 retry;
- 请求进入历史后再次失败的 retry;
- 模型报错;
- 拦截 prompt 的 UserPromptSubmit hook;
- 拦截一次的 Stop hook,以及一直拦截到上限的 Stop hook;
@@ -426,6 +449,9 @@ PR 3–6 每一项之后,同样各自跟随默认开启和删除旧 loop 两
- 从 PR 2c-1 起,当前轮次在最后一个回复之后才到达的后台结果,会开启一个后台通知轮次,而不是让 prompt 继续;Todo Stop Guard 生效时,中途排队的输入成为下一个 prompt,而不是让这一个继续;
- Todo Stop Guard 只在 Stop hook 允许回复停下后才继续,并且使用单独的请求,不再与 hook 的理由合并;Stop hook 运行期间到达的输入不再作废它的决定;
- Stop hook 和 Todo Stop Guard 的续发沿用这一轮的 prompt id,与 TUI 的续发一样运行 UserPromptSubmit;除 Live 和 Agent Host 会话保留 ACP 的工具调用守卫外,运行 client 的守卫;并像任何一批工具一样把工具结果发回;
- 从 PR 2c-2 起,channel prompt 和 Goal 提案轮次在共享路径上得到与全新 prompt 相同的处理,包括日期提醒、系统提醒和 client 的召回;
- retry、continue 和 restore 会开启 client 的一次交互:循环检测器、探索预算和轮次预算重新开始,它们的第一轮不计入会话轮数上限;
- continue 的第一条请求一直没有发出时(被取消或失败),被打断的用户输入按原样放回,而不是作为一条合并后的条目保留,合并条目取走的 worktree 提醒也留给下一轮;
ACP 对拍测试覆盖了保持不变的常见场景,并对日期提醒、工具结果上的 UserPromptSubmit、该 hook 运行时取消,以及一批的审批先于它的调用执行做了断言。
@@ -25,7 +25,7 @@ Explicitly out of scope for this design (moves to later phases on request): publ
| Provider HTTP additions | `provision`, `observe` | + `storage:provision`, `storage:observe`, publication/capture (§5, §7 reuse wire discipline) |
| Workspace state machine | — | same state machine as LOCAL/SSH_LOCAL with new ownership kind `EXTERNAL` |
The profile is selected by `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER=external-workspace` in a deployment where Workspace files and/or shell are explicitly enabled. The phase‑1 scratch profile and its validation (`validateExternalRuntime`) remain untouched.
The profile is selected by `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER=external-workspace` in a deployment where Workspace files and/or shell are explicitly enabled. The phase‑1 scratch profile and its validation rules (now owned by `ExternalRuntimeProviderFactory.validate`) remain untouched.
## 3. Identity and wire contract (shared transport discipline)
@@ -25,7 +25,7 @@
| Provider HTTP 补充 | `provision`, `observe` | 追加 `storage:provision`、`storage:observe`、publication/capture(§5/§7) |
| Workspace 状态机 | — | 与 LOCAL/SSH_LOCAL 同状态机,ownership 新增 `EXTERNAL` kind |
部署侧通过 `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER=external-workspace` 显式开启,且相应 Workspace files/shell 同时被配置打开。阶段一的 scratch 档案与 `validateExternalRuntime` 校验保持不变。
部署侧通过 `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER=external-workspace` 显式开启,且相应 Workspace files/shell 同时被配置打开。阶段一的 scratch 档案及其校验规则保持不变(现由 `ExternalRuntimeProviderFactory.validate` 承担)。
## 3. 身份与持久化契约(延续阶段一)
@@ -109,12 +109,22 @@ The `database` row reads the Flyway scripts from both server jars: the manifest'
Spring resolves `qwen.managed-agent.runtime-broker.state-directory` from many places: the variable the server's `application.yml` maps, `QWEN_MANAGED_AGENT_RUNTIME_STATE_DIRECTORY`; every configuration file in Spring's default locations and the launcher's `SPRING_CONFIG_ADDITIONAL_LOCATION`; the files an import or a profile brings in; `SPRING_APPLICATION_JSON`; system properties; command-line arguments; and every relaxed spelling of the property. Earlier rounds of this command read that list itself and refused on what it recognized, and each round found a source it missed. Its configuration resolution has no closed list, so the command no longer reads it.
Instead, `check` and `backup` ask the server. The server jar has a main class, `ResolvedConfiguration`, that prepares the environment the way `SpringApplication` does before it creates a context: the arguments, the process environment and every environment post-processor on the class path, which load the configuration files, imports and profiles. It then binds the server's properties and prints the state directory, made absolute in the working directory as the server does, where it came from, the auth mode and the trusted actor header, on one line after a fixed prefix. It starts no context, logs nothing and opens no connection.
Instead, `check` and `backup` ask the server. The server jar has a main class, `ResolvedConfiguration`, that prepares the environment the way `SpringApplication` does before it creates a context: the arguments, the process environment and every environment post-processor on the class path, which load the configuration files, imports and profiles. It then binds the server's properties and prints the state directory, made absolute in the working directory as the server does, where it came from, the auth mode, the trusted actor header and the listener URLs described below, on one line after a fixed prefix. It starts no context, logs nothing and opens no connection.
The command runs it through `systemd-run` as the control-plane unit runs the server: the unit's account, working directory, environment files and `Environment=` assignments, the `java` on the unit's `PATH`, and the launcher's one default, `config/application.yml` when `SPRING_CONFIG_ADDITIONAL_LOCATION` is unset or empty. It runs this release's jar, copied where the service account can read it wherever the archive was extracted, through `PropertiesLauncher` with `loader.main`, so an older jar without the class fails rather than starting a server. `verify`, which runs after `install`, uses the installed jar and reads the auth mode from the same output. The admin launcher runs the same class as `resolved-configuration`, for the manual procedure on other layouts.
The command runs it through `systemd-run` as the control-plane unit runs the server: the unit's account, working directory, environment files and `Environment=` assignments, the `java` on the unit's `PATH`, and the launcher's one default, `config/application.yml` when `SPRING_CONFIG_ADDITIONAL_LOCATION` is unset or empty. It runs this release's jar, copied where the service account can read it wherever the archive was extracted, through `PropertiesLauncher` with `loader.main`, so an older jar without the class fails rather than starting a server. `start` and `verify`, which run after `install`, use the installed jar; `verify` reads the auth mode from the same output. The admin launcher runs the same class as `resolved-configuration`, for the manual procedure on other layouts.
Two cases stay with the operator. The jar can fail to resolve the configuration, as when an import names a missing file, which the server would refuse too. And `systemctl show` prints `ExecStart` arguments joined by spaces, unquoted, so the command cannot pass on arguments the unit gives the launcher. In either case `backup` refuses until `--runtime-state <directory>` names the directory, or `--runtime-state none` declares a deployment that keeps no Runtime state. A backup without the Runtime state the database refers to cannot be restored consistently, so a backup that silently left it out would be worse than a refusal. When the configuration names no directory, as on a deployment without the tool path, `backup` archives none and prints that.
### Listener URLs
`start` waits for the Harness's port and for the control plane's health endpoint, and `verify` probes that endpoint and reads the API. Each address can be set where `server.env` does not show it: `server.port` in `config/application.yml`, a relaxed `SERVER_ADDRESS`, an argument, or a `management.server.port` that moves the actuator to a listener of its own. Read from `server.env` alone, `start` would wait out `--timeout` on a port nothing serves. So both phases take the URLs from the same `ResolvedConfiguration` output:
- **Control plane:** the address `server.address` binds, the port `server.port` binds and `https` when `server.ssl` is enabled. A wildcard bind is reached on the loopback interface of its family, and any other address as bound.
- **Health endpoint:** Spring Boot's rules for the management port (`ManagementPortType`). Unset or equal to the server's port, it shares the server's listener. A negative one turns the HTTP endpoints off. Any other one opens a listener on `management.server.address`, which binds every interface when unset, under `management.server.base-path`, keeping the server's TLS unless `management.server.ssl` replaces it. `management.endpoints.web.base-path` and the `health` path mapping apply either way, and a blank mapping keeps the name `health`, as Spring Boot ignores it.
- **Harness:** `qwen.managed-agent.harness.base-url`, the URL the control plane dials.
A port of 0, picked at start, or a negative one leaves no URL to wait on, so both phases refuse until `--server-url` names one. They refuse as well when a URL cannot name the listener, as for a link-local IPv6 address, which keeps its zone, or a `--server-url` that is not a URL, rather than wait out `--timeout` on probes that cannot be sent. `--server-url` still names the control plane, for example behind a proxy, and the health endpoint is then `<url>/actuator/health`; `verify`, which does not reach the Harness, then resolves the configuration only for `--tenant`. When the configuration does not resolve, in the two cases above, the two phases use the URLs `server.env` names, as the command did before, and print a line that says which: a wrong guess then costs a `--timeout`, where refusing would stop an upgrade that the guess serves.
### Backup freshness
`backup.json` records these facts:
@@ -160,7 +170,7 @@ The control-plane unit adds `SuccessExitStatus=143`. `check` reports the changed
- Flyway ordering;
- the validation hook;
- the validation `databaseCheck` runs, on an H2 database Flyway migrated.
- **`ResolvedConfiguration` unit tests**, through Spring's own preparation: an import in the launcher's file, a flow mapping with sibling keys, a merge key, a profile file beside a directory location, a properties file chosen by `SPRING_CONFIG_NAME`, every environment spelling, `SPRING_APPLICATION_JSON`, a relative directory, the auth mode and actor header, a missing import, and the printed line.
- **`ResolvedConfiguration` unit tests**, through Spring's own preparation: an import in the launcher's file, a flow mapping with sibling keys, a merge key, a profile file beside a directory location, a properties file chosen by `SPRING_CONFIG_NAME`, every environment spelling, `SPRING_APPLICATION_JSON`, a relative directory, the auth mode and actor header, the listener URLs for each port, address, TLS and management setting above, a missing import, and the printed line.
- **End to end on a real host** (systemd 257, MySQL 8.4, the real Hosted Harness with a fake model). The old release was installed the way `systemd/README.md` describes, with a stand-in worker left in the server cgroup.
- **Compatible upgrade:** every phase, plus these checks:
- the refusals for running and out-of-order phases;
@@ -109,12 +109,22 @@
Spring 会从许多地方解析 `qwen.managed-agent.runtime-broker.state-directory`:服务端 `application.yml` 所映射的变量 `QWEN_MANAGED_AGENT_RUNTIME_STATE_DIRECTORY`;Spring 默认位置和 launcher 的 `SPRING_CONFIG_ADDITIONAL_LOCATION` 中的每个配置文件;import 或 profile 引入的文件;`SPRING_APPLICATION_JSON`;系统属性;命令行参数;以及该属性在 relaxed binding 下的每种写法。本命令前几轮自己读取这份清单,并对识别出的来源拒绝执行,结果每一轮都发现有遗漏。Spring 的配置解析没有一份封闭的清单,因此命令不再自己读取。
改为由 `check` 和 `backup` 询问服务端。服务端 jar 中有一个主类 `ResolvedConfiguration`,它按 `SpringApplication` 在创建 context 之前的方式准备环境:命令行参数、进程环境,以及 class path 上的所有 environment post-processor,由它们加载配置文件、import 和 profile。随后它绑定服务端的属性,在固定前缀之后用一行打印:状态目录(与服务端一样按工作目录转成绝对路径)及其来源、认证模式和可信 actor 请求头。它不启动 context,不输出日志,也不建立任何连接。
改为由 `check` 和 `backup` 询问服务端。服务端 jar 中有一个主类 `ResolvedConfiguration`,它按 `SpringApplication` 在创建 context 之前的方式准备环境:命令行参数、进程环境,以及 class path 上的所有 environment post-processor,由它们加载配置文件、import 和 profile。随后它绑定服务端的属性,在固定前缀之后用一行打印:状态目录(与服务端一样按工作目录转成绝对路径)及其来源、认证模式、可信 actor 请求头,以及下文所述的监听 URL。它不启动 context,不输出日志,也不建立任何连接。
命令通过 `systemd-run`,按控制面 unit 运行服务端的方式运行它:使用该 unit 的账号、工作目录、环境文件和 `Environment=` 赋值,以及 unit `PATH` 上的 `java`,并带上 launcher 唯一的默认值,即 `SPRING_CONFIG_ADDITIONAL_LOCATION` 未设置或为空时使用 `config/application.yml`。运行的是本版本的 jar:它被复制到服务账号能读取的位置,因此不受压缩包解压位置的影响;通过 `PropertiesLauncher` 加 `loader.main` 启动,因此没有该类的旧 jar 会直接失败,而不会启动一个服务端。`verify` 在 `install` 之后运行,使用已安装的 jar,并从同一份输出读取认证模式。admin 启动脚本以 `resolved-configuration` 命令运行同一个类,供其他布局的手工流程使用。
命令通过 `systemd-run`,按控制面 unit 运行服务端的方式运行它:使用该 unit 的账号、工作目录、环境文件和 `Environment=` 赋值,以及 unit `PATH` 上的 `java`,并带上 launcher 唯一的默认值,即 `SPRING_CONFIG_ADDITIONAL_LOCATION` 未设置或为空时使用 `config/application.yml`。运行的是本版本的 jar:它被复制到服务账号能读取的位置,因此不受压缩包解压位置的影响;通过 `PropertiesLauncher` 加 `loader.main` 启动,因此没有该类的旧 jar 会直接失败,而不会启动一个服务端。`start` 和 `verify` 在 `install` 之后运行,使用已安装的 jar;`verify` 从同一份输出读取认证模式。admin 启动脚本以 `resolved-configuration` 命令运行同一个类,供其他布局的手工流程使用。
有两种情况留给运维处理。一是 jar 无法解析配置,例如 import 指向的文件不存在,这时服务端同样会拒绝启动。二是 `systemctl show` 打印 `ExecStart` 参数时用空格拼接且不加引号,命令无法准确地转交 unit 传给 launcher 的参数。这两种情况下,`backup` 都会拒绝执行,直到 `--runtime-state <目录>` 指明目录,或用 `--runtime-state none` 表示部署不保存 Runtime 状态。缺少数据库所引用的 Runtime 状态的备份无法一致地恢复,因此悄悄漏掉状态的备份比拒绝更糟。配置中没有指定目录时(例如未开启工具路径的部署),`backup` 不归档 Runtime 状态,并打印说明。
### 监听 URL
`start` 等待 Harness 的端口和控制面的健康端点,`verify` 探测该端点并读取 API。这些地址都可能设置在 `server.env` 看不到的地方:`config/application.yml` 中的 `server.port`、relaxed 形式的 `SERVER_ADDRESS`、命令行参数,或者把 actuator 移到独立监听上的 `management.server.port`。只从 `server.env` 推算时,`start` 会在一个无人监听的端口上等满 `--timeout`。因此两个阶段都从同一份 `ResolvedConfiguration` 输出中取得 URL:
- **控制面:** `server.address` 绑定的地址、`server.port` 绑定的端口;启用 `server.ssl` 时用 `https`。通配地址经其地址族的回环接口访问,其他地址按绑定的地址访问。
- **健康端点:** 遵循 Spring Boot 关于 management 端口的规则(`ManagementPortType`)。未设置或与服务端端口相同时,它与服务端共用监听;为负数时关闭 HTTP 端点;其他值则在 `management.server.address` 上打开独立监听(未设置时绑定所有接口),位于 `management.server.base-path` 之下,并沿用服务端的 TLS,除非 `management.server.ssl` 替换它。两种情况下都应用 `management.endpoints.web.base-path` 和 `health` 的路径映射;路径映射为空白时 Spring Boot 会忽略它,仍使用名称 `health`。
- **Harness:** `qwen.managed-agent.harness.base-url`,即控制面访问它所用的 URL。
端口为 0(启动时选取)或负数时没有可等待的 URL,因此两个阶段都会拒绝执行,直到 `--server-url` 指定一个。URL 无法表示该监听时(例如保留了 zone 的 IPv6 链路本地地址),或者 `--server-url` 不是 URL 时,两个阶段同样拒绝执行,而不是在根本发不出去的探测上等满 `--timeout`。`--server-url` 仍可指定控制面的地址,例如位于代理之后,此时健康端点为 `<url>/actuator/health`;`verify` 不访问 Harness,这时只为 `--tenant` 解析配置。无法解析配置时(即上文的两种情况),两个阶段与以前一样使用 `server.env` 中给出的 URL,并打印一行说明用了哪些:猜错的代价是等满一次 `--timeout`,而拒绝执行会挡住猜对时本可以完成的升级。
### 备份是否仍然有效
`backup.json` 记录以下内容:
@@ -160,7 +170,7 @@ Spring 会从许多地方解析 `qwen.managed-agent.runtime-broker.state-directo
- Flyway 顺序;
- 校验钩子;
- `databaseCheck` 实际执行的校验,在 Flyway 迁移出的 H2 数据库上运行。
- **`ResolvedConfiguration` 单元测试**,走 Spring 自己的环境准备:launcher 配置文件中的 import、带同级键的 flow mapping、merge key、目录位置旁的 profile 文件、`SPRING_CONFIG_NAME` 选中的 properties 文件、各种环境变量写法、`SPRING_APPLICATION_JSON`、相对路径的目录、认证模式与 actor 请求头、缺失的 import,以及打印的那一行。
- **`ResolvedConfiguration` 单元测试**,走 Spring 自己的环境准备:launcher 配置文件中的 import、带同级键的 flow mapping、merge key、目录位置旁的 profile 文件、`SPRING_CONFIG_NAME` 选中的 properties 文件、各种环境变量写法、`SPRING_APPLICATION_JSON`、相对路径的目录、认证模式与 actor 请求头、上述每种端口、地址、TLS 和 management 设置下的监听 URL、缺失的 import,以及打印的那一行。
- **真实主机端到端**(systemd 257、MySQL 8.4,真实 Hosted Harness 配合伪造模型)。旧版本按 `systemd/README.md` 的方式安装,并在服务端 cgroup 中留一个替身 worker。
- **兼容的升级:** 跑完所有阶段,并验证:
- 在服务运行时或阶段顺序不对时的拒绝;
@@ -8,6 +8,8 @@ remaining acceptance work. The investigation used `523be857db`. This design supe
boundary in the [source layout](2026-10-15-managed-agent-server-layout.md) and
[Java package design](2026-10-19-managed-agent-package-boundaries.md).
The five-backend extraction and the updated dependency matrix are defined in the [Runtime backend module design](2026-10-31-managed-runtime-backend-modules.md). The matrix below records the original #15871 boundary.
## Problem and constraints
The baseline has 391 production Java sources, 614 test Java sources and 18 Maven
@@ -8,6 +8,8 @@
[源码布局](2026-10-15-managed-agent-server-layout.zh-CN.md)及
[Java 包设计](2026-10-19-managed-agent-package-boundaries.zh-CN.md)中的单模块边界。
五个后端的进一步拆分及更新后的依赖矩阵见 [Runtime 后端模块设计](2026-10-31-managed-runtime-backend-modules.zh-CN.md)。下表记录 #15871 最初建立的边界。
## 问题与约束
基线包含 391 个生产 Java 源文件、614 个测试 Java 源文件和 18 个 Maven profile。
+114
View File
@@ -0,0 +1,114 @@
# Mod prompt submission
[English](2026-10-30-mod-prompt-submit.md) | [简体中文](2026-10-30-mod-prompt-submit.zh-CN.md)
Status: proposed. This stage follows the turn events in #15443 and is tracked in #13749. It follows the Claude Code 2.1.310 plugin API declarations of `prompt.submit` and `$.prompt.submit`.
## Problem and current behavior
Mods written for Claude Code act on the prompts a person sends:
- A guard drops a prompt that holds a credential, with `{ drop: reason }`.
- A Mod rewrites a prompt, for example to expand a shorthand, with `next({ ...e, text })`.
- A Mod attaches context the model reads beside the prompt and the person never sees, with `next({ ...e, context: [...] })`.
- A Mod sends a prompt of its own once the session is idle, with `$.prompt.submit`.
Qwen Code skips `prompt.submit` hooks at load and reports `Hooks for unsupported events will not run: prompt.submit`. A guard written there silently does not run.
## Goals and non-goals
Goals, in four pull requests:
1. Core and headless runs: the event, its rules and its context. This document lands with it.
2. The Ink terminal.
3. The OpenTUI terminal.
4. `$.prompt.submit`.
Non-goals:
- Slash commands, which Claude Code raises with `command`. They come with a later step on command rows.
- Prompts the person did not type: background notifications, scheduled prompts, teammate messages, goal continuations and the continuation of an interrupted turn. Claude Code marks some of these with `isMeta`.
- Prompts typed while a turn runs that steer it at a model boundary.
- `attachments` and `$.prompt.media`.
## Proposed changes
### 1. When it fires
`prompt.submit` fires for each prompt a person submits, after the host has its text and before anything reads it: before UserPromptSubmit, before `@` references are read and before the user message is recorded or shown. Only a host that raises the event loads its hooks; any other host skips them at startup with the usual line, so a guard is never silently bypassed. Step 1 raises it in headless runs, for the `-p` prompt and each stream-json user message.
### 2. Input
| Field | Value |
| --------- | ---------------------------------------------------------------------------------------------- |
| `text` | the prompt as submitted, with pasted text expanded; for a stream-json message, its text blocks |
| `context` | absent as the host raises it |
| `wait` | `false` |
| `origin` | `{ kind: 'sdk' }` in headless runs; `{ kind: 'composer' }` in the terminals |
| `turnId` | absent until a terminal raises the event for a prompt typed during a turn |
A hook passes `text` and `context` on to `next`; `origin`, `wait` and `turnId` are pinned. `context` is a list of texts.
### 3. What enters
The prompt that enters is the one that arrives beneath the hooks, as in Claude Code: the worker records the `text` and `context` that reach the bottom of the chain. `next(e)` resolves to them with the origin. What a hook answers after its `next` resolved changes nothing, and only a drop there is checked.
The chain settles one of three ways:
- the prompt arrived beneath the hooks: it enters with its text and context;
- a hook answered `{ drop: reason }`: the prompt is dropped by a hook;
- the chain settled without reaching the bottom, because a hook answered `{ text }` without `next`: the prompt is not submitted, as Claude Code's `a hook answered without passing the prompt on`.
A hook that answers a drop after calling `next` fails, as in Claude Code: its `.catch` is asked, or it is skipped and its last `next` stands. A hook that fails or times out is skipped, and a failed worker lets the prompt through unchanged, so a broken plugin never blocks a prompt.
### 4. Rewrite and context
A rewritten text replaces the prompt everywhere the host keeps it: the text sent to the model, the text `@` processing reads, the `turn.start` text, the transcript record and its display text. A stream-json message that also has blocks other than text reaches the model as text with those blocks serialized; the hooks hear only its text blocks, and a rewrite replaces them, with the other blocks after it, in the model's text and in the transcript alike, as without a rewrite.
The context joins UserPromptSubmit's `additionalContext` in the hidden block core already appends after the prompt, ahead of it, escaped the same way. The client takes it as a new send option, `promptContext`, which only the prompt's own UserQuery spends. The transcript records the block as hook context, so resume hides it and compaction keeps it the way it keeps UserPromptSubmit's.
### 5. Drop
A drop stops the prompt before UserPromptSubmit and before any model request, and starts no turn. The reason is one line, kept to its first 4,096 characters with an ellipsis after, as Claude Code keeps it. Headless runs end with the error result `Prompt dropped by a hook: <reason>`, or `Prompt not submitted: a hook answered without passing the prompt on`, and exit code 1. A stream-json session goes on to its next message.
### 6. Limits
A prompt longer than half a Mod payload (128 KiB as JSON) reaches no hook, and the line `prompt.submit hook skipped: the prompt is too long for a Mod payload` appears where transcript logs do. Claude Code passes it whole. A rewrite or context that makes a hook's answer larger than a Mod payload fails that hook.
## Design decisions
- **The host raises the event.** Each host already holds the submitted text before it expands or records it. Raising the event there keeps the rewrite ahead of every use of the text without changing core's send.
- **UserPromptSubmit runs after the Mod hooks, in core.** Claude Code runs both in one admission and lets a settings hook's block beneath turn `next(e)` into `{ drop }`. Here the bottom of the chain does not wait for UserPromptSubmit, which runs later in the send and can take as long as its commands. A Mod that must know whether UserPromptSubmit blocked a prompt can read `turn.start`, which only an admitted prompt raises.
- **Hosts opt in.** The load request says whether the host raises `prompt.submit`, as it names the sites it draws for `ui.render`, so each step enables only the host it adds.
- **The context shares the hook context block.** It reaches the model, the transcript and resume the way UserPromptSubmit's context does, so no new record kind is needed. A context the conversation already holds word for word is not sent again, as for UserPromptSubmit. Each Mod context and UserPromptSubmit's count on their own, so a Mod context that changes every turn does not make an unchanged UserPromptSubmit context repeat.
## Differences from Claude Code
These go into the user guide:
- Until steps 2 and 3, only headless runs raise `prompt.submit`.
- Slash commands and prompts the person did not type raise none, so `command` and `isMeta` never appear.
- `attachments` never appears, and `$.prompt.media` does not exist.
- A UserPromptSubmit block does not turn `next(e)` into `{ drop }`, and `next(e)` resolves before the turn starts.
- A context the conversation already holds word for word is not sent again, each Mod context and UserPromptSubmit's on its own.
- A prompt longer than 128 KiB as JSON reaches no hook.
- A headless drop is an error result with exit code 1.
## Validation and acceptance
- Real QuickJS worker tests:
- which plugins hear the event, and a host that raises none skipping its hooks;
- the prompt and context that arrive beneath the hooks, and an answer after `next` that changes nothing;
- a hook's drop, and a hook that answers without `next`;
- a downstream drop's reason survives a post-next text answer across plugins, within one plugin, and during recovery; a caught late drop does not replace a later local continuation's outcome or mark a host continuation as called;
- a drop after `next`, a changed `origin` or `wait`, a context that is not a list, a wrong answer and a throw, each skipped;
- a `.catch` that drops for a failed guard.
- Mod session tests: no hooks, the origin, a rewrite with context, a drop's one line and its 4,096 characters, a failed worker, a cancelled prompt, and a prompt too long for a payload.
- Client test: the Mods' context ahead of UserPromptSubmit's, escaped, with and without UserPromptSubmit hooks, and only for a new prompt.
- Headless tests: the rewritten prompt and its context reaching the send and `turn.start`, a drop in JSON and text, and no event for a slash command or a notification.
- The built CLI against a fixture server in headless JSON, text and stream-json: a rewrite that the model request and the transcript both hold, context in the hidden block, a drop, and a guard whose `.catch` drops.
## Risks and open questions
- A slow hook delays the prompt by up to its budget, 10 seconds a hook.
- Masking a slash command's arguments needs `command.run` today; `prompt.submit` with `command` comes later.
@@ -0,0 +1,114 @@
# Mod 提示提交
[English](2026-10-30-mod-prompt-submit.md) | [简体中文](2026-10-30-mod-prompt-submit.zh-CN.md)
状态:提议中。本阶段接在 #15443 的回合事件之后,由 #13749 跟踪。依据是 Claude Code 2.1.310 插件 API 对 `prompt.submit` 和 `$.prompt.submit` 的声明。
## 问题与现状
为 Claude Code 写的 Mod 会处理用户发出的提示:
- 守卫用 `{ drop: reason }` 拦下含有凭据的提示。
- Mod 用 `next({ ...e, text })` 改写提示,例如展开缩写。
- Mod 用 `next({ ...e, context: [...] })` 附上模型能读到、用户看不到的上下文。
- Mod 用 `$.prompt.submit` 在会话空闲后发出自己的提示。
Qwen Code 在加载时跳过 `prompt.submit` hook,并提示 `Hooks for unsupported events will not run: prompt.submit`。写在这里的守卫不会运行。
## 目标与非目标
目标,分四个 PR:
1. core 和 headless:事件本身、规则和上下文。本文随它合入。
2. Ink 终端。
3. OpenTUI 终端。
4. `$.prompt.submit`。
非目标:
- 斜杠命令。Claude Code 对它们带 `command` 触发,留到后续处理命令行的步骤。
- 不是用户输入的提示:后台通知、定时提示、队友消息、goal 续跑、中断回合的续跑。Claude Code 对其中一部分标 `isMeta`。
- 回合进行中输入、在模型边界插入当前回合的提示。
- `attachments` 和 `$.prompt.media`。
## 方案
### 1. 触发时机
用户每提交一个提示,在宿主拿到文本之后、任何地方读取它之前触发 `prompt.submit`:在 UserPromptSubmit 之前,在读取 `@` 引用之前,在记录或显示用户消息之前。只有会触发该事件的宿主才加载它的 hook;其他宿主在启动时照常跳过并给出提示,守卫不会被悄悄绕过。第 1 步在 headless 运行中触发:`-p` 的提示和 stream-json 的每条用户消息。
### 2. 输入
| 字段 | 值 |
| --------- | ------------------------------------------------------------ |
| `text` | 提交的提示,粘贴内容已展开;stream-json 消息只取其文本块 |
| `context` | 宿主触发时不带 |
| `wait` | `false` |
| `origin` | headless 为 `{ kind: 'sdk' }`;终端为 `{ kind: 'composer' }` |
| `turnId` | 不带,直到终端为回合进行中输入的提示触发该事件 |
hook 可以把改过的 `text` 和 `context` 传给 `next`;`origin`、`wait`、`turnId` 不可改。`context` 是文本列表。
### 3. 最终提交什么
和 Claude Code 一样,提交的是穿过所有 hook 到达链底的提示:worker 记录到达链底的 `text` 和 `context`。`next(e)` 解析为它们,外加 origin。hook 在 `next` 返回之后再给出的答复不起作用,这时只检查它是不是 drop。
链的结果有三种:
- 提示到达链底:带着它的文本和上下文提交;
- 某个 hook 答复 `{ drop: reason }`:提示被 hook 丢弃;
- 链没有到达链底就结束了,因为某个 hook 没调 `next` 就答复了 `{ text }`:提示没有提交,对应 Claude Code 的 `a hook answered without passing the prompt on`。
和 Claude Code 一样,调过 `next` 之后再答复 drop 的 hook 算失败:先问它的 `.catch`,否则跳过它,保留它最后一次 `next` 的结果。失败或超时的 hook 被跳过;worker 出错时提示原样通过。坏掉的插件不会拦住提示。
### 4. 改写与上下文
改写后的文本在宿主保存提示的每一处都替换掉原文:发给模型的文本、`@` 处理读取的文本、`turn.start` 的文本、transcript 记录及其显示文本。stream-json 消息若还带有非文本块,这些块会序列化后随文本发给模型;hook 只听到其中的文本块,改写替换的也是文本块,其他块接在改写后的文本之后;发给模型的文本和 transcript 都是这样,与没有改写时一致。
上下文并入 core 已有的隐藏块,即追加在提示之后、存放 UserPromptSubmit `additionalContext` 的那一块,排在后者之前,转义方式相同。client 通过新的发送选项 `promptContext` 接收它,只有该提示自己的 UserQuery 会用掉它。transcript 把这一块记作 hook 上下文,因此恢复会话时它仍然隐藏,压缩时也和 UserPromptSubmit 的上下文一样保留。
### 5. 丢弃
drop 在 UserPromptSubmit 和任何模型请求之前拦下提示,不开始回合。原因只保留一行,最多 4,096 个字符,超出部分以省略号结尾,与 Claude Code 相同。headless 运行以错误结果 `Prompt dropped by a hook: <reason>`(或 `Prompt not submitted: a hook answered without passing the prompt on`)结束,退出码为 1。stream-json 会话继续处理下一条消息。
### 6. 限制
超过 Mod 负载一半(JSON 128 KiB)的提示不经过任何 hook,并在 transcript 日志出现的位置给出 `prompt.submit hook skipped: the prompt is too long for a Mod payload`。Claude Code 会完整传递。改写或上下文让 hook 的答复超过 Mod 负载时,该 hook 算失败。
## 设计决策
- **由宿主触发事件。** 每个宿主在展开或记录提示之前就已拿到提交的文本。在这里触发,改写就能先于文本的每一次使用,且不必改 core 的发送流程。
- **UserPromptSubmit 在 Mod hook 之后、在 core 里运行。** Claude Code 把两者放在同一次准入里,下层设置 hook 的拦截会让 `next(e)` 变成 `{ drop }`。这里链底不等 UserPromptSubmit:它在发送流程更靠后的位置运行,耗时取决于它的命令。需要知道 UserPromptSubmit 是否拦下提示的 Mod 可以看 `turn.start`,只有获准的提示才会触发它。
- **宿主自行声明。** 加载请求说明宿主是否触发 `prompt.submit`,就像它为 `ui.render` 列出绘制的位置一样。每一步只为它新增的宿主开启。
- **上下文共用 hook 上下文块。** 它到达模型、进入 transcript、在恢复时的处理都和 UserPromptSubmit 的上下文一致,不需要新的记录类型。会话里已有一字不差的同一上下文时不再发送,与 UserPromptSubmit 相同。每条 Mod 上下文和 UserPromptSubmit 的上下文分别判断,所以每轮都变的 Mod 上下文不会让没变的 UserPromptSubmit 上下文重复发送。
## 与 Claude Code 的差异
以下内容写进用户指南:
- 第 2、3 步之前,只有 headless 运行触发 `prompt.submit`。
- 斜杠命令和不是用户输入的提示不触发它,所以不会出现 `command` 和 `isMeta`。
- 不会出现 `attachments`,也没有 `$.prompt.media`。
- UserPromptSubmit 的拦截不会让 `next(e)` 变成 `{ drop }`;`next(e)` 在回合开始之前就返回。
- 会话里已有一字不差的同一上下文时不再发送,每条 Mod 上下文和 UserPromptSubmit 的上下文分别判断。
- 超过 JSON 128 KiB 的提示不经过任何 hook。
- headless 的 drop 是错误结果,退出码为 1。
## 验证与验收
- 真实 QuickJS worker 测试:
- 哪些插件收到该事件,以及不触发它的宿主跳过它的 hook;
- 到达链底的提示和上下文,以及 `next` 之后的答复不起作用;
- hook 的 drop,以及不调 `next` 就答复的 hook;
- 跨插件、同一插件内和恢复路径中,`next` 返回后的文本答复都保留下层 drop 的原因;被捕获的迟到 drop 不覆盖后续本地 continuation 的结果,也不把宿主 continuation 标记为已调用;
- `next` 之后 drop、改动 `origin` 或 `wait`、上下文不是列表、答复格式错误、抛错,均被跳过;
- 守卫失败时由 `.catch` 丢弃。
- Mod session 测试:没有 hook、origin、带上下文的改写、drop 的单行和 4,096 字符、worker 出错、提示被取消、超过负载的提示。
- client 测试:Mod 的上下文排在 UserPromptSubmit 的上下文之前并已转义;有无 UserPromptSubmit hook 两种情况;只用于新提示。
- headless 测试:改写后的提示和上下文到达发送和 `turn.start`;JSON 和文本模式下的 drop;斜杠命令和通知不触发事件。
- 构建后的 CLI 对接 fixture 服务器,在 headless JSON、文本和 stream-json 下验证:模型请求和 transcript 都是改写后的文本;上下文在隐藏块里;drop;守卫失败时由 `.catch` 丢弃。
## 风险与待定问题
- 慢 hook 会让提示延迟,最多为它的预算,每个 hook 10 秒。
- 目前遮蔽斜杠命令的参数要靠 `command.run`;带 `command` 的 `prompt.submit` 留到以后。
@@ -0,0 +1,257 @@
# Hosted A6 — block on uncertain producer evidence
Status: implemented. Slice of Issue #14481 (G3 Step 3 recovery matrix), row **A6**
("MCP/Hook/child uncertainty"). Per the parent design's §9.1, A6 stays focused or
integration evidence: this slice adds focused behavioural evidence, guards every
recovery writer against uncertain child evidence, and keeps a bounded cancel
escape for the resulting block. No new CI arm, no new continuation support, no
change to the Hook/MCP refusal policy itself.
Parent: [G3 Step 3 recovery](2026-10-09-hosted-g3-step3-recovery.md).
中文版:[2026-10-31-hosted-a6-uncertainty-evidence.zh-CN.md](2026-10-31-hosted-a6-uncertainty-evidence.zh-CN.md).
## Problem
A6 requires three observations: missing or unknown producer evidence blocks; no
external call is recreated; no Workspace lease is silently released. An
investigation of main `35daec35ea` (two parallel code surveys, key claims
re-verified by hand) shows the second and third clauses are already thickly
guarded and tested, while the first clause has three concrete evidence gaps:
- **G1 — the Hook/MCP saved-batch refusal arm has zero tests.**
`resumeSavedNativeBatch` refuses a non-commandStep plan owned by a Hook/MCP
session with `HostedToolRecoveryRequiredError('Unsupported saved batch owner.')`
(`hosted-workspace-tool-turn.ts:3508-3517`). The string appears nowhere in any
test; the approval-continuation fixtures never construct a hooks/mcp resume.
Deleting the clause is predicted to keep every suite green.
- **G2 — the extensions-enabled entry combination is unpinned.** The takeover
continuation entry (`hosted-harness-session.ts:7124-7129`) admits sessions with
`session.extensions?.enabled` even when `session.hooks || session.mcp`; the only
backstop for a non-commandStep plan is G1's untested clause. (Resolution scope:
this slice pins the tool-turn backstop in both gate directions; the entry's
`extensions?.enabled` clause itself is a route-level surface recorded as a
follow-up below — a focused fixture cannot reach it without a full
hooks-session takeover, and a row that cannot fail is worse than no row.)
- **G3 — an unavailable child ledger is answered as proven absence.**
`fillParkedRoundAgentGaps` folds `gapText` for a stranded call when
`children === undefined || admitted === undefined`
(`hosted-runtime-recovery.ts:1505-1517`). The two arms are not the same evidence:
with the ledger present, no `child_run` record is positive never-started proof
(`admitLaunch` commits the record before `dispatchStarted`,
`hosted-child-agent-session.ts:1383-1415`); with the ledger absent, nothing is
known — yet the fold asserts "the call never ran"
(`HOSTED_AGENT_CALL_NOT_REACHED_TEXT`, `hosted-workspace-tool-turn.ts:1660-1661`).
The ledger family is `agent` AND `workflow`: the module's other readers already
treat the two names as one (`hosted-runtime-recovery.ts:790, 1913`,
`JOURNAL_ONLY_ANSWERABLE_TOOLS`), and workflow admission requires the child
funnel. `session.childAgents` is rebuilt deterministically from persisted
session configuration at load (`hosted-harness-session.ts:6265-6307`), so the
absent arm is unreachable for a stable configuration that never had the agent
tool — but it is reachable across child-lane configuration/version drift
between the crashed generation and the recovering one (reproducible at route
level via `suppressChildAgents` on a version-0 lane), exactly when an admitted
child may still be running. No test distinguishes the two arms.
## Verified code facts (main 35daec35ea)
- Producer evidence is durable and pre-dispatch for all three producers: Hook
`hook_execution` records and MCP `mcp_operation` records run
`intent → dispatch_started → settled | outcome_unknown | not_started_proven`,
with `dispatch_started` committed before the physical dispatch
(`hosted-hook-session.ts:1187-1193, 1362-1378`;
`hosted-mcp-session.ts:865-882`); child runs commit `child_run` before dispatch.
- Uncertain Hook/MCP results are lookup-only (`hook-status` / status polling);
non-settled lookups commit `recovery_blocked / outcome_unknown` and throw the
family recovery error. No path recreating an uncertain external call was found
(dispatch only from `intent`; `reconcileEffects` cancels intents, never
redispatches; child admission is evidence-idempotent).
- Clause 3 is guarded by persistent evidence, not tool lists: Java release
refuses while any execution row is non-terminal
(`JdbcToolExecutionRepository.hasActiveByRuntimeSession`), descendant half
evidence blocks retirement (`ManagedRuntimeRetentionGuard.childRunUnsettled`),
and the CLI fast-settle path requires a non-empty all-settled list. Tested
witnesses include `hosted-mcp-session.test.ts:846, 1762` (unknown outcome keeps
the owner / blocks replacement), `hosted-hook-session.test.ts:892` (unknown
result keeps the mount), and `HostedMonitorRunFaultGatesIT` (uncertain worker →
`runtime_session_held`, binding stays READY).
## Changes
- **G1/G2 (tests only).** A five-row owner-gate matrix in
`hosted-approval-continuation.test.ts`: a non-commandStep saved plan under a
hooks-owned and under an mcp-owned turn is refused with the
`'Unsupported saved batch owner.'` cause; a hooks owner with
`extensionConfig` and no commandStep is refused by the same first arm; a
commandStep plan whose `configRevision` no longer matches the turn's
`extensionConfig.revision` is refused by the gate's second arm; and a
matching-revision commandStep plan under a hooks owner proceeds PAST the gate
(its rejection is the wrapped transport failure, never the owner cause) —
pinning the over-blocking direction that keeps the A3 commandStep follow-up
working. The gap is demonstrated first: deleting the owner-gate clause keeps
the pre-change suites green.
- **G3 (recovery writers + a bounded cancel escape).** On a live signal, an
owed fold for a ledger-family call (`agent` or `workflow`) with
`children === undefined` refuses in `fillParkedRoundAgentGaps`,
`fillParkedRoundCommandGaps`, and the hosted resumed-turn answer. The common
`HostedChildLedgerRecoveryRequiredError` subtype marks exactly this evidence
block for the route. Already-journaled gaps stay silent, non-family gaps keep
their existing answers, and a present ledger's missing record remains positive
never-started proof. Pre-aborted terminating fills and the `'all'` settlement
write `HOSTED_CHILD_LEDGER_UNKNOWN_TEXT` with `executionStatus: 'unknown'`,
never a fabricated never-ran or cancelled outcome. Route witnesses build the
drift into the load (`suppressChildAgents` on the orphan parked shape):
continue blocks retriably with no fabricated fold, then managed-runtime cancel
settles that same blocked Turn, writes the cancelled terminal, clears the
recovery block, and lets the public cancel replay answer 204. A different
recovery block still refuses cancel with 409 `turn_blocked`.
## Design decisions
- **Block, not fold, for an unavailable ledger.** A6's first clause is about
evidence semantics: "unknown" must not be laundered into "proven absent". The
fold is durable and answers the model; a wrong never-ran claim can strand a
live child's effect with no owed response.
- **The ledger family, not just `agent`.** `workflow` launches ride the same
child-run ledger (the module's other readers treat the two names as one
family), so the same unavailable-evidence block covers both. Other stranded
calls keep the existing gap answer, so the guard cannot regress sessions
that legitimately have no child funnel.
- **Abort exemption for the terminating routes.** Cancel and the interrupted
settle funnel deliberately pre-abort the fill ("settles at once … instead of
holding the cancel hostage"); the guard exempts an already-aborted signal so
those paths always terminate. Their ledger-less family folds say the outcome
is unknown and retain the record; they never claim the call never ran or was
observed cancelled.
- **A bounded terminal escape after the block.** Only the A6 subtype arms
`recoveryCancelPromptId`; only that Turn's managed-runtime cancel bypasses
the blocked refusal. The failed continue's admission is removed so it cannot
replay as a cancellation, and the block lifts only after a cancelled terminal
is durable. Other recovery blocks keep their 409 refusal and lease behavior.
- **No packaged arm, no continuation support.** §9.1 classifies A6 as focused or
integration evidence, and native continuation refuses Hook/MCP sessions by
design (the commandStep follow-up is A3's other half). Packaging a Hook/MCP
crash arm would need new e2e fixtures and is a separate slice.
- **Lease-side observations stay out of scope.** Hook/MCP-profile Turns do not
enter the HOSTED_CLEANUP debt protocol (`!hooks && !mcp` gates), so the C3
scanner does not re-drive their leases; that is a cleanup-coverage question
(a stuck lease is not a silent release), recorded here as an adjacent
observation for a future C3 slice, not an A6 defect.
## Files affected
- `packages/cli/src/serve/hosted-runtime-recovery.ts` (G3 writer guards and the
A6 error subtype)
- `packages/cli/src/serve/hosted-harness-session.ts` (the bounded cancel escape)
- `packages/cli/src/serve/hosted-child-wait-recovery.test.ts` (G3 fill-level cases)
- `packages/cli/src/serve/hosted-background-launch-recovery.test.ts` (the
journal-only workflow/ledger refusal)
- `packages/cli/src/serve/hosted-harness-session.test.ts` (G3 route-level cases)
- `packages/cli/src/serve/hosted-approval-continuation.test.ts` (G1/G2 matrix)
- this design doc (both languages); the parent §9.1 status note is updated when
the focused evidence lands.
## Acceptance criteria
- G1/G2: the owner-gate deletion survives the pre-change suites (demonstrated
gap) and reds the refusal rows afterwards; removing `!plan.commandStep &&`
reds exactly the matching-revision acceptance row; removing the second
(revision) arm reds exactly the stale-revision row. The harness entry's
`extensions?.enabled` clause is NOT claimed as pinned (follow-up below).
- G3: the absent-ledger counterexample folds "never ran" before the guard and
throws with no fold written after it, for both `agent` and `workflow`; the
ledger-present/no-record arm still folds the honest answer; a non-family gap
call is unchanged; a journaled gap replays silently. Pre-aborted fills and the
`'all'` settlement write the unknown outcome; the settlement's own answering
pass pins `executionStatus: 'unknown'`. At route level under built-in drift:
continue blocks retriably (409) with no fabricated fold, then cancel settles
that same Turn and clears the block; an unrelated recovery block still
refuses cancel. Each new guard or escape clause has a delete-control that
reds its named case.
- Focused suites green under a sanitized environment (agent-session env vars
stripped), typecheck/ESLint/Prettier green; production changes stay limited to
the recovery writers and the bounded cancel escape.
## Open questions
- Pinning the harness takeover entry's `session.extensions?.enabled` clause at
route level (needs a hooks-session takeover fixture with a durable v3
continuation; the tool-turn backstop it gates is already pinned both ways).
- Whether a later slice packages an A6 fault arm once Hook/MCP e2e fixtures
exist (out of scope here).
- Whether Hook/MCP-profile Turn leases should join the cleanup-debt protocol
(adjacent C3-scope observation above).
## R2 review round (4 Suggestions — all fixed)
- **Sibling writers (R2-1)**: the same never-ran fabrication survived at two
sibling writers on the recovery paths. `answerAbandonedTurnCalls` (hosted
mode, the continue route's resumed-turn answer) now refuses a ledger-less
family call — its caller's family catch maps the refusal to the retriable
block; the terminating `'all'` settlement keeps folding but on the honest
wording (below). `fillParkedRoundCommandGaps` (which never reads the
ledger) refuses the same shape on a live signal; its continue-route caller
passes the live signal, the cancel and settle callers pre-abort.
- **Honest exempted wording (R2-3)**: the exempted (pre-aborted) folds and the
`'all'` settlement answer a ledger-less family call with a new dedicated
`HOSTED_CHILD_LEDGER_UNKNOWN_TEXT` — "outcome is unknown … record is
retained", `executionStatus: 'unknown'` — never "never ran" and never
"cancelled". The funnels still terminate without throwing.
- **One family fact (R2-4)**: `CHILD_LEDGER_TOOL_NAMES = new Set(['agent',
'workflow'])` lives in `hosted-workspace-tool-turn.ts` beside the answer
constants; all six former inline two-name literals (four in recovery, two
in the tool-turn mount-validation arms) read `.has(name)` from it. Not
`JOURNAL_ONLY_ANSWERABLE_TOOLS` — that set also holds the team names whose
gap answer is their own funnel.
- **The extensions row (R2-2)**: the owner-gate matrix gained the row its
comment claimed — a Hook owner with `extensionConfig` and NO `commandStep`
is refused by the first arm; smuggling `&& !this.extensionConfig` into the
gate reds exactly that row.
R2 delete-controls (quiet tree, failure-list adjudication): the family
constant hollowed to `agent`-only at the fill guard; the resumed-answer
refusal removed; the `'all'` settlement's unknown wording reverted; the
command-gaps refusal removed; the extensions exemption smuggled into the
gate — every row KILLED at exactly its named case, restores byte-identical.
## R3 verification round (1 Major + 3 Suggestions — all fixed)
- **Terminal escape (F1)**: the new route sequence was reproduced red before
the fix — continue blocks, then managed-runtime cancel answers 409. Only the
A6 subtype now arms a same-Turn cancel exemption; the failed continue's
admission is deleted so it cannot replay as a cancel, and a durable cancelled
terminal lifts the block. The witness asserts the unknown fold, cancelled
terminal, cleared status, and public-cancel 204. The pre-existing generic
recovery-block control still answers 409 `turn_blocked`.
- **Settlement status (F2)**: the settle case now selects `call-3` — the call
answered by the settlement's own pass, not a sibling fill — and asserts its
`executionStatus: 'unknown'`.
- **Debug residue (F3)**: the command-wait case's projection `console.log` is
removed.
- **Stale description (F4)**: the PR body and both design versions describe
the five-row matrix, all recovery writers, honest unknown folds, and the
bounded cancel escape rather than the pre-R2 single-guard shape.
R3 delete-controls (quiet tree, one selected test per row): the first blocked
gate, second blocked gate, continue-admission deletion, terminal block lift,
and A6 subtype each red the block-then-cancel witness; broadening the marker
to every family error reds exactly the generic blocked-cancel control;
reverting the settlement status reds exactly the `call-3` case. All 7 rows
KILLED, every restore sha256-identical.
Gates: 8 focused suites, 1587/1587 (the harness file's 686 tests completed in
two explicit groups, 175 + 511, after the default full-file run reached the
10-minute runner limit without a test failure); typecheck RC=0; ESLint and
Prettier green.
## R4 verification follow-up (1 Suggestion — fixed)
- **Journal-only workflow refusal**: the verifier's sole surviving mutation,
M12, narrowed `settleJournalOnlyInterruptedTurn`'s family test to
`call.name === 'agent'`. The existing `parkYoloWithOwedCall` fixture now has
a workflow sibling: with no child ledger, the takeover must keep
`{kind: 'declined', reason: 'model_start'}` and leave the input unsettled.
This is test-only; production code is unchanged.
- M12 now reds exactly that case (`1 failed | 11 passed`, received
`{kind: 'inapplicable'}`), and the restored suite is 12/12. The cumulative
focused evidence is 9 suites, 1599 tests (the previous 1587 plus this 12-test
journal-only suite).
@@ -0,0 +1,194 @@
# Hosted A6 — 不确定的 producer 证据必须阻塞
状态:已实现。Issue #14481(G3 Step 3 恢复矩阵)的 **A6** 行切片
(「MCP/Hook/child 不确定性」)。按父设计 §9.1,A6 属定向或集成证据:本切片
补充定向行为化证据,在每个恢复写入点守卫不确定 child 证据,并为由此产生的
阻塞保留有边界的 cancel 出口。不新增 CI 臂、不新增续接支持、不改 Hook/MCP
拒绝策略本身。
父文档:[G3 Step 3 恢复](2026-10-09-hosted-g3-step3-recovery.zh-CN.md)。
English: [2026-10-31-hosted-a6-uncertainty-evidence.md](2026-10-31-hosted-a6-uncertainty-evidence.md).
## 问题
A6 要求三个观察:缺失或未知的 producer 证据必须阻塞;不重建外部调用;不静默
释放 Workspace 租约。对 main `35daec35ea` 的调查(两路并行代码调研,关键主张
已手工复核)表明:第二、三子句已有厚实守卫与测试,第一子句存在三个具体证据
缺口:
- **G1 — Hook/MCP 保存批次拒绝臂零测试。** `resumeSavedNativeBatch` 对
Hook/MCP 会话拥有的非 commandStep 计划抛
`HostedToolRecoveryRequiredError('Unsupported saved batch owner.')`
(`hosted-workspace-tool-turn.ts:3508-3517`)。该字符串在任何测试中都不出现;
approval-continuation 夹具从不构造 hooks/mcp 的 resume。预计删掉该子句后所有
套件仍绿。
- **G2 — extensions 开启的入口组合未钉。** 接管续接入口
(`hosted-harness-session.ts:7124-7129`)在 `session.extensions?.enabled` 时
即使 `session.hooks || session.mcp` 也放行;非 commandStep 计划的唯一兜底就是
G1 那条未测子句。(解决范围:本切片双向钉住 tool-turn 兜底;入口的
`extensions?.enabled` 子句本身是路由级表面,记录为下方后续工作——定向夹具
无法在不构造完整 hooks 会话接管的情况下触达它,而一个无法失败的用例比没有
更糟。)
- **G3 — 不可得的 child 账本被当作「证明缺席」应答。**
`fillParkedRoundAgentGaps` 在 `children === undefined || admitted === undefined`
时对滞留调用折叠 `gapText`(`hosted-runtime-recovery.ts:1505-1517`)。两臂的
证据语义不同:账本在场时,无 `child_run` 记录是「从未启动」的肯定证据
(`admitLaunch` 先提交记录再 `dispatchStarted`,
`hosted-child-agent-session.ts:1383-1415`);账本缺席时一无所知——折叠却断言
「the call never ran」(`HOSTED_AGENT_CALL_NOT_REACHED_TEXT`,
`hosted-workspace-tool-turn.ts:1660-1661`)。账本家族是 `agent` 与 `workflow`
两者:本模块其他读取点已把两个名字视为一家
(`hosted-runtime-recovery.ts:790, 1913`、`JOURNAL_ONLY_ANSWERABLE_TOOLS`),
且 workflow 准入要求 child funnel。`session.childAgents` 在加载时由
持久会话配置确定性重建(`hosted-harness-session.ts:6265-6307`),因此对从未
拥有 agent 工具的稳定配置,缺席臂不可达——但在崩溃代与恢复代之间发生
child 通道配置/版本漂移时可达(路由级可用 version-0 通道上的
`suppressChildAgents` 复现),而那恰恰可能有一个已 admit 的 child 仍在运行。
没有测试区分这两臂。
## 已核实的代码事实(main 35daec35ea)
- 三类 producer 的证据都是持久且先于 dispatch 的:Hook `hook_execution` 与 MCP
`mcp_operation` 记录走
`intent → dispatch_started → settled | outcome_unknown | not_started_proven`,
`dispatch_started` 在物理 dispatch 之前提交
(`hosted-hook-session.ts:1187-1193, 1362-1378`;
`hosted-mcp-session.ts:865-882`);child run 先提交 `child_run` 再 dispatch。
- 不确定的 Hook/MCP 结果只走 lookup(`hook-status`/状态轮询);非 settled 的
lookup 提交 `recovery_blocked / outcome_unknown` 并抛家族恢复错误。未发现任何
重建不确定外部调用的路径(dispatch 仅从 `intent`;`reconcileEffects` 只取消
intent、绝不重发;child admission 证据幂等)。
- 第三子句由持久证据而非工具列表守卫:Java 释放在任何执行行非终态时拒绝
(`JdbcToolExecutionRepository.hasActiveByRuntimeSession`);后代半证据阻塞
退役(`ManagedRuntimeRetentionGuard.childRunUnsettled`);CLI 快速结清要求
非空且全 settled 的列表。已有测试见证包括
`hosted-mcp-session.test.ts:846, 1762`(未知结果保留 owner/阻断替换)、
`hosted-hook-session.test.ts:892`(未知结果保留 mount)、
`HostedMonitorRunFaultGatesIT`(不确定 worker → `runtime_session_held`,
binding 保持 READY)。
## 改动
- **G1/G2(仅测试)。** 在 `hosted-approval-continuation.test.ts` 增加五行
owner 门矩阵:hooks 拥有与 mcp 拥有的 turn 恢复非 commandStep 保存计划时以
`'Unsupported saved batch owner.'` cause 拒绝;Hook owner 带
`extensionConfig` 且无 commandStep 也被第一臂拒绝;`configRevision` 与 turn 的
`extensionConfig.revision` 不再匹配的 commandStep 计划由门的第二臂拒绝;
版本匹配的 commandStep 计划在 hooks owner 下越过该门(其拒绝是被包装的
传输失败,绝非 owner cause)——钉住保住 A3 commandStep 后续可用的过度阻塞
方向。先实证缺口:删除 owner 门子句后改动前的套件仍绿。
- **G3(恢复写入点 + 有边界的 cancel 出口)。** 在存活信号下,仍欠折叠的
账本家族调用(`agent` 或 `workflow`)若 `children === undefined`,
`fillParkedRoundAgentGaps`、`fillParkedRoundCommandGaps` 与 hosted
resumed-turn 应答都拒绝。共同的
`HostedChildLedgerRecoveryRequiredError` 子类型只为这种证据阻塞做路由标记。
已入日志缺口保持静默,非家族缺口保留既有应答,账本在场但无记录仍是
「从未启动」的肯定证据。预中止的终止性填充与 `'all'` 结算写入
`HOSTED_CHILD_LEDGER_UNKNOWN_TEXT` 和 `executionStatus: 'unknown'`,绝不伪造
never-ran 或 cancelled 结果。路由见证把漂移建进加载(orphan 停靠形状 +
`suppressChildAgents`):continue 可重试地阻塞且无伪造折叠,随后
managed-runtime cancel 结算同一个被阻塞 Turn、写入 cancelled 终态、清除恢复
阻塞,公开 cancel 重放应答 204。其他恢复阻塞仍以 409 `turn_blocked` 拒绝
cancel。
## 设计决定
- **账本不可得时阻塞而非折叠。** A6 第一子句是证据语义问题:「未知」不得被
洗成「证明缺席」。折叠是持久的、要回答模型的;错误的 never-ran 断言会让一个
存活 child 的效果搁浅且无人欠它应答。
- **账本家族,不止 `agent`。** `workflow` 启动走同一本 child-run 账本(本模块
其他读取点已把两个名字视为一家),因此同一不可得证据阻塞覆盖两者。其他
滞留调用保留既有缺口应答,守卫不会回归合法没有 child funnel 的会话。
- **终止路由的 abort 豁免。** cancel 与中断结算漏斗刻意预中止该填充
(「立刻结算……而不是把 cancel 扣为人质」);守卫豁免已中止信号,使这两条
路径永远可终止。无账本家族折叠明确说结果未知并保留记录,绝不声称调用从未
运行或已观察到取消。
- **阻塞后的有边界终止出口。** 只有 A6 子类型会设置
`recoveryCancelPromptId`,也只有该 Turn 的 managed-runtime cancel 能越过
blocked 拒绝。失败的 continue admission 被删除,不能冒充 cancel 重放;只有
cancelled 终态持久化后才解除阻塞。其他恢复阻塞保留 409 拒绝与租约行为。
- **不打包臂、不加续接支持。** §9.1 把 A6 归为定向或集成证据,且 native
continuation 按设计拒绝 Hook/MCP 会话(commandStep 后续是 A3 的另一半)。
打包 Hook/MCP 崩溃臂需要新的 e2e 夹具,属独立切片。
- **租约侧观察留在范围外。** Hook/MCP profile 的 Turn 不入 HOSTED_CLEANUP 债务
协议(`!hooks && !mcp` 门),C3 扫描器不代还其租约;这是清理覆盖问题
(租约卡住不等于静默释放),在此记录为未来 C3 切片的相邻观察,而非 A6 缺陷。
## 涉及文件
- `packages/cli/src/serve/hosted-runtime-recovery.ts`(G3 写入点守卫与 A6 错误子类型)
- `packages/cli/src/serve/hosted-harness-session.ts`(有边界的 cancel 出口)
- `packages/cli/src/serve/hosted-child-wait-recovery.test.ts`(G3 fill 级用例)
- `packages/cli/src/serve/hosted-background-launch-recovery.test.ts`(journal-only
workflow/账本拒绝)
- `packages/cli/src/serve/hosted-harness-session.test.ts`(G3 路由级用例)
- `packages/cli/src/serve/hosted-approval-continuation.test.ts`(G1/G2 矩阵)
- 本设计文档(双语);定向证据落地时同步更新父文档 §9.1 状态注记。
## 验收标准
- G1/G2:owner 门删除在改动前的套件中存活(已实证的缺口),改动后使拒绝行
变红;移除 `!plan.commandStep &&` 恰好使版本匹配接受行变红;移除第二
(版本)臂恰好使版本过期行变红。不声称钉住 harness 入口的
`extensions?.enabled` 子句(见下方后续工作)。
- G3:缺席账本反例在守卫前折叠「never ran」,守卫后对 `agent` 与 `workflow`
均抛错且不写任何折叠;账本在场/无记录臂仍折叠诚实应答;非家族缺口调用
不变;已入日志缺口静默重放;预中止填充与 `'all'` 结算写入未知结果,并在
结算自己的应答臂上钉住 `executionStatus: 'unknown'`。路由级内建漂移下:
continue 可重试地阻塞(409)且无伪造折叠,随后 cancel 结算同一 Turn 并解除
阻塞;无关恢复阻塞仍拒绝 cancel。每个新增守卫或出口子句都有使命名用例变红
的删除对照。
- 定向套件在净化环境(剥离 agent 会话环境变量)下绿;typecheck/ESLint/Prettier
绿;生产改动只涉及恢复写入点与有边界的 cancel 出口。
## 开放问题
- 在路由级钉住 harness 接管入口的 `session.extensions?.enabled` 子句(需要
带持久 v3 续接的 hooks 会话接管夹具;它所把守的 tool-turn 兜底已双向钉住)。
- 待 Hook/MCP e2e 夹具存在后,是否由后续切片打包 A6 故障臂(此处范围外)。
- Hook/MCP profile Turn 的租约是否应加入清理债务协议(上文相邻 C3 范围观察)。
## R2 评审轮(4 条建议——全部修复)
- **兄弟写入点(R2-1)**:同一 never-ran 伪造在恢复路径的两个兄弟写入点上仍然存活。`answerAbandonedTurnCalls`(hosted 模式,即 continue 路由的 resumed-turn 应答)现在对无账本家族调用直接拒绝——调用方的家族 catch 把拒绝映射为可重试阻塞;终止性的 `'all'` 结算继续折叠,但使用诚实措辞(见下)。从不读账本的 `fillParkedRoundCommandGaps` 在存活信号下拒绝同一形状;其 continue 调用方传存活信号,cancel 与 settle 调用方预中止。
- **诚实的豁免措辞(R2-3)**:被豁免(预中止)的折叠与 `'all'` 结算对无账本家族调用改用新的专用 `HOSTED_CHILD_LEDGER_UNKNOWN_TEXT`——「结果未知……记录保留」、`executionStatus: 'unknown'`——绝不写「never ran」也绝不写「cancelled」。漏斗仍不抛错、照常终止。
- **家族单一事实源(R2-4)**:`CHILD_LEDGER_TOOL_NAMES = new Set(['agent', 'workflow'])` 定义在 `hosted-workspace-tool-turn.ts` 的应答常量旁;原先六处内联双名字字面量(recovery 四处、tool-turn 挂载校验两处)全部改读 `.has(name)`。不复用 `JOURNAL_ONLY_ANSWERABLE_TOOLS`——那套还包含团队名,其缺口应答是各自的 funnel。
- **extensions 行(R2-2)**:owner 门矩阵补上了注释所声称的那一行——Hook owner + `extensionConfig` + 无 `commandStep` 被第一臂拒绝;向门里偷运 `&& !this.extensionConfig` 恰好使该行变红。
R2 删除对照(安静树、完整失败清单判定):把家族常量在守卫处挖空为仅 `agent`;移除 resumed-answer 拒绝;把 `'all'` 结算的 unknown 措辞改回;移除 command-gaps 拒绝;向门里偷运 extensions 豁免——每行恰好杀于命名用例,还原逐字节一致。
## R3 验证轮(1 条 Major + 3 条建议——全部修复)
- **终止出口(F1)**:修复前按新路由顺序实证变红——continue 阻塞后,
managed-runtime cancel 应答 409。现在只有 A6 子类型会为同一 Turn 设置
cancel 豁免;失败的 continue admission 被删除,不能冒充 cancel 重放;
cancelled 终态持久化后才解除阻塞。见证断言未知结果折叠、cancelled 终态、
状态清锁与公开 cancel 204。既有普通 recovery-block 对照仍应答 409
`turn_blocked`。
- **结算状态(F2)**:结算用例现在精确选择 `call-3`——由结算自己的应答臂
处理、而非兄弟填充处理的调用——并断言其 `executionStatus: 'unknown'`。
- **调试残留(F3)**:删除命令等待用例的 projection `console.log`。
- **过期描述(F4)**:PR 正文与双语设计文档都改为描述五行矩阵、全部恢复
写入点、诚实未知折叠与有边界的 cancel 出口,不再是 R2 之前的单守卫形状。
R3 删除对照(安静树、每行只选一个用例):第一道 blocked 门、第二道
blocked 门、continue admission 删除、终态清锁与 A6 子类型各自使
「先阻塞再 cancel」见证变红;把标记扩大到所有家族错误恰好只使普通
blocked-cancel 对照变红;回退结算状态恰好只使 `call-3` 用例变红。7 行全部
KILLED,每次还原 sha256 一致。
门禁:8 个定向套件 1587/1587(harness 文件的 686 个用例在默认全文件运行
无断言失败但达到 10 分钟执行上限后,分成 175 + 511 两个显式组完成);
typecheck RC=0;ESLint 与 Prettier 绿。
## R4 验证跟进(1 条建议——已修复)
- **journal-only workflow 拒绝**:验证者唯一存活的变异 M12 把
`settleJournalOnlyInterruptedTurn` 的家族判断缩为
`call.name === 'agent'`。现有 `parkYoloWithOwedCall` 夹具现在增加 workflow
兄弟用例:没有 child 账本时,接管必须保持
`{kind: 'declined', reason: 'model_start'}` 且输入仍未结算。这是纯测试补钉,
生产代码不变。
- M12 现在恰好只使该用例变红(`1 failed | 11 passed`,实际收到
`{kind: 'inapplicable'}`),还原后套件 12/12。累计定向证据为 9 个套件、
1599 个用例(此前 1587 + 本 journal-only 套件 12)。
@@ -69,26 +69,29 @@ exclusion; zero `TODO`/`FIXME` markers in production code.
**D1. God classes.** The largest production classes:
| Class | Module | Lines | Notes |
| ---------------------------------- | -------------- | ----- | ------------------------------------------------------------------------------------------------------------ |
| `ManagedAgentStore` | persistence | 7,751 | 300+ methods, 7 public setters |
| `RuntimeBrokerService` | runtime-broker | 6,329 | 230+ methods; cooldown and hold inventory extracted (`ExecutionObservationCooldown`, `RuntimeHoldInventory`) |
| `ManagedSessionStore` | persistence | 3,931 | |
| `WorkspaceCsiApplicationInventory` | persistence | 3,319 | |
| `ToolPublicationDataStore` | persistence | 2,709 | |
| `QwenHostedHarnessConnector` | adapters | 2,328 | |
| `ManagedAgentService` | control-plane | 2,120 | |
| `HarnessCoordinator` | control-plane | 1,770 | |
| Class | Module | Lines | Notes |
| ---------------------------------- | -------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ManagedAgentStore` | persistence | 7,657 | 300+ methods; dependencies constructor-injected and final (#16061) |
| `RuntimeBrokerService` | runtime-broker | 4,270 | ~150 methods; binding registry, execution reconciler/recovery/cancellation/dispatcher, support/state, cooldown and hold inventory extracted (see P1) |
| `ManagedSessionStore` | persistence | 3,931 | |
| `WorkspaceCsiApplicationInventory` | persistence | 3,319 | |
| `ToolPublicationDataStore` | persistence | 2,709 | |
| `QwenHostedHarnessConnector` | adapters | 2,328 | |
| `ManagedAgentService` | control-plane | 2,120 | |
| `HarnessCoordinator` | control-plane | 1,770 | |
`ManagedAgentStore` mixes session admission, turns, events, item/message
projection, worktrees, MCP management, definition pins, skills, and extension
projection. Its 7 public `set*`/`configure*` methods are mutable post-
construction wiring: the bean can be observed before it is fully assembled.
`RuntimeBrokerService` mixes provisioning, binding CAS, leases, cancellation,
recovery scans, and extension commands in a single class; the observation
cooldown and the tool-hold inventory are the first concerns delegated to
extracted collaborators (see the P1 extraction slices below). Tests mirror
the shape: `RuntimeBrokerServiceTest` is 8,002
projection. Its dependencies are now constructor-injected into `final` fields
(#16061), with `@Nullable` optional collaborators and a `@Lazy` record-store
cycle, so the bean can no longer be observed half-assembled.
`RuntimeBrokerService` originally mixed provisioning, binding CAS, leases,
cancellation, recovery scans, and extension commands in a single class; the
P1 extraction slices have since delegated the binding registry, execution
reconciliation/recovery/cancellation/dispatcher, support/state, observation
cooldown and tool-hold inventory to nine module-internal collaborators,
leaving a 4,270-line facade (see the P1 extraction slices below). Tests mirror
the shape: `RuntimeBrokerServiceTest` is 7,992
lines and `HarnessCoordinatorTest` is 4,910 lines.
**D2. Telescoping session-creation API.** `insertWorkspaceSessionCommand` has
@@ -225,21 +228,24 @@ cross-cutting "how the system works today" tour.
projection, turn/message queries, worktrees, definitions/skills — sharing
the same `JdbcTemplate` and transaction manager. The module README already
permits connection-sharing SQL helpers to stay in one package.
Replace the 7 public setters with constructor injection; express any real
cycle with `ObjectProvider` or `@Lazy` instead of mutable wiring. Split the
mirrored test files in the same slices. Setter investigation (2026-10-31):
four of the seven setters are Spring `@Autowired(required = false)`
optional-dependency injection (`lifecycle`, `definitions`,
`sessionWorktrees`, `journalEventTransport`) and three are required plain
`@Autowired` injection (`extensionPackages`, `creationSkills`,
`worktreeFiles` — its bean is supplied by
`ManagedAgentConfiguration.managedWorktreeFiles()`; the direct calls in
`VerificationFixtures` are test setup, not production wiring). Converting
them to constructor `ObjectProvider` injection is a large change to this
central 7,700-line class with ~30 test wiring sites, and its correctness
depends on Spring runtime assembly that the manual-setter unit tests do
not fully exercise; it warrants a dedicated design pass and full
integration verification before implementation, not an incremental slice.
The constructor-injection prerequisite is done (#16061): the 7 public
setters were replaced by a single `@Autowired` constructor holding `final`
references, with `@Nullable` optional collaborators and a `@Lazy`
record-store cycle, and the mirrored verification fixtures were migrated.
Setter investigation (2026-10-31, before the conversion): four of the seven
setters were Spring `@Autowired(required = false)` optional-dependency
injection (`lifecycle`, `definitions`, `sessionWorktrees`,
`journalEventTransport`) and three were required plain `@Autowired`
injection (`extensionPackages`, `creationSkills`, `worktreeFiles` — its bean
was supplied by `ManagedAgentConfiguration.managedWorktreeFiles()`; the
direct calls in `VerificationFixtures` were test setup, not production
wiring). The dedicated [constructor-wiring design](2026-11-01-managed-agent-store-wiring.md)
covers the required/optional dependencies, the additional lazy record-store
field and its Session-store cycle, final references, direct-fixture migration
and real Spring context verification. It is implemented locally with focused
verification passed and full CI qualification pending; the conversion touched
this central 7,657-line class and ~30 test wiring sites. The remaining work is
the physical aggregate split itself, each aggregate its own slice.
- `RuntimeBrokerService`: extract module-internal collaborators for recovery
scanning, cancellation, tool holds, and observation cooldown; the service
becomes a facade. The module already exists, so no cross-module contract
@@ -249,36 +255,40 @@ cross-cutting "how the system works today" tour.
no lifecycle of its own. The tool-hold inventory followed the same day
(`RuntimeHoldInventory`): the durable pagination and RELEASED filter move
verbatim, while the service keeps the admission, tenant resolution and
error-mapping boundary. Recovery scanning was investigated next and proved
error-mapping boundary. Recovery scanning was then investigated and proved
entangled — `scanExecutions` drives `reconcileInContext`, and the
background-process settle family owns the `SessionContext` in-memory index
and the CAS settle loop — so extracting it now would create a collaborator
with a dozen injected callbacks rather than a clean seam; it waits for the
reconciliation path to become its own collaborator. Cancellation follows
the same rule: extract only where the seam is clean. Known coverage gap
carried into the reconciliation slice:
the stamp-before-in-flight-slot-drop ordering in `lookupOnce` has no test
pinning it (a pre-existing gap, identical at the base); pin it when the
reconciliation path becomes its own collaborator and the ordering is a
seam-level contract rather than a private lambda's internal sequence.
- Reconciliation, recovery scanning and cancellation are **not** clean
standalone seams (investigated 2026-10-31). Unlike the cooldown and the
hold inventory — which own their state — these paths share the service's
core mutable state and lifecycle machinery: the `sessions`, `liveBindings`
and `reconciliations` concurrent maps, the binding-lifecycle helpers
(`requireAnswerableBinding`, `requireLiveBinding`, `invalidateBinding`,
`provisioner.isUsable`), and the CAS execution settle
(`absorbRuntimeStatus` → `settleUnstartedBackgroundSiblings`). Extracting
any of them now would inject a dozen maps/helpers/callbacks into a
"collaborator" that is the god class under another name. The prerequisite
is a **shared-state registry** slice: move the three maps and the
binding-lifecycle helpers behind a `RuntimeBindingRegistry` (or similar)
that owns them, leaving the service to delegate. Once that registry
exists, reconciliation/recovery/cancellation depend on a narrow owner
instead of the service's internals and become extractable, and the M6
ordering becomes a seam-level contract a test can pin. This registry
slice is larger and riskier than the two clean seams and should be
designed and reviewed on its own before implementation.
and the CAS settle loop — so a standalone seam would have needed a dozen
injected callbacks. That cluster has since been extracted through the
[Runtime Binding Registry](2026-10-31-runtime-binding-registry.md) and the
[execution workflow extraction](2026-11-01-runtime-execution-workflows.md):
reconciliation, recovery scanning and cancellation now live in
`RuntimeExecutionReconciler`, `RuntimeExecutionRecovery` and
`RuntimeExecutionCancellation`, so nothing in the cluster is still waiting.
Known coverage gap carried into the reconciliation slice: the
stamp-before-in-flight-slot-drop ordering in `lookupOnce` had no test pinning
it at the base. The
[workflow extraction](2026-11-01-runtime-execution-workflows.md) now adds a
deterministic blocked-stamp regression at the reconciler boundary; its
verification evidence is recorded with that increment.
- Reconciliation, recovery scanning and cancellation were **not** clean
standalone seams (investigated 2026-10-31). Their prerequisite was the
[Runtime Binding Registry](2026-10-31-runtime-binding-registry.md) first
slice: it owns `liveBindings`, `bindingOperations`, `sessions`, local
Session contexts and Binding validation/invalidation helpers. The source
audit corrected the original three-map sketch: `bindingOperations` had to
move with Binding lifecycle operations, whereas execution-keyed
`reconciliations` stayed with `ExecutionObservationCooldown` and moved in
the later reconciliation slice. In the Registry slice the service retained
provisioning, drain, recovery, execution settlement, renewal and scheduler
lifecycle. This
established a concrete owner without injecting service callbacks. The M6
stamp-before-slot-removal coverage gap is addressed by the blocked-stamp
regression in the [execution workflow extraction](2026-11-01-runtime-execution-workflows.md),
which implements reconciliation, takeover scans and cancellation after
the Registry. The Registry slice itself does not claim that coverage.
See the linked bilingual designs for concurrency, cleanup and acceptance
requirements; local and CI qualification are recorded separately.
- Apply the same treatment to `ManagedSessionStore` and
`QwenHostedHarnessConnector` afterwards, each as its own slice.
@@ -60,24 +60,25 @@ Managed Agent 服务端从首篇设计(2026-09-19)起,约两个月内长
**D1. 上帝类。** 最大的生产类:
| 类 | 模块 | 行数 | 备注 |
| ---------------------------------- | -------------- | ----- | ------------------------------------------------------------------------------------------- |
| `ManagedAgentStore` | persistence | 7,751 | 300+ 方法,7 个公开 setter |
| `RuntimeBrokerService` | runtime-broker | 6,329 | 230+ 方法;冷却与 hold 清单已提取(`ExecutionObservationCooldown`、`RuntimeHoldInventory`) |
| `ManagedSessionStore` | persistence | 3,931 | |
| `WorkspaceCsiApplicationInventory` | persistence | 3,319 | |
| `ToolPublicationDataStore` | persistence | 2,709 | |
| `QwenHostedHarnessConnector` | adapters | 2,328 | |
| `ManagedAgentService` | control-plane | 2,120 | |
| `HarnessCoordinator` | control-plane | 1,770 | |
| 类 | 模块 | 行数 | 备注 |
| ---------------------------------- | -------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `ManagedAgentStore` | persistence | 7,657 | 300+ 方法;依赖已构造注入并为 final(#16061) |
| `RuntimeBrokerService` | runtime-broker | 4,270 | ~150 方法;binding registry、execution reconciler/recovery/cancellation/dispatcher、support/state、冷却与 hold 清单已提取(见 P1) |
| `ManagedSessionStore` | persistence | 3,931 | |
| `WorkspaceCsiApplicationInventory` | persistence | 3,319 | |
| `ToolPublicationDataStore` | persistence | 2,709 | |
| `QwenHostedHarnessConnector` | adapters | 2,328 | |
| `ManagedAgentService` | control-plane | 2,120 | |
| `HarnessCoordinator` | control-plane | 1,770 | |
`ManagedAgentStore` 混合了会话准入、turn、event、item/message 投影、worktree、
MCP 管理、definition pin、skills 与 extension 投影。它的 7 个公开
`set*`/`configure*` 方法是构造后的可变装配:Bean 在完全装配好之前就可能被
观察到。`RuntimeBrokerService` 把 provisioning、绑定 CAS、租约、取消、恢复
扫描与 extension 命令揉在一个类里;observation 冷却与 tool hold 清单是
首批委托给已提取协作者的关注点(见下方 P1 提取切片)。测试
镜像了同样的形状:`RuntimeBrokerServiceTest` 8,002 行,
MCP 管理、definition pin、skills 与 extension 投影。它的依赖现已构造注入到
`final` 字段(#16061),可选协作者用 `@Nullable`、record-store 回环用
`@Lazy`,Bean 不再可能在半装配状态下被观察到。`RuntimeBrokerService` 原本把 provisioning、绑定 CAS、租约、取消、恢复
扫描与 extension 命令揉在一个类里;P1 提取切片已把 binding registry、
execution 对账/恢复/取消/分发、support/state、observation 冷却与 tool hold
清单委托给九个模块内协作者,留下 4,270 行的门面(见下方 P1 提取切片)。测试
镜像了同样的形状:`RuntimeBrokerServiceTest` 7,992 行,
`HarnessCoordinatorTest` 4,910 行。
**D2. 望远镜式会话创建 API。** `insertWorkspaceSessionCommand` 在
@@ -195,18 +196,20 @@ sweep 管理这个陷阱。各字段的单位本身是一份被钉住的绑定
- `ManagedAgentStore`:按聚合拆分——准入/命令、事件与投影、turn/message
查询、worktree、definition/skills——共享同一个 `JdbcTemplate` 与事务
管理器。模块 README 已允许共享连接的 SQL helper 留在同一包。用构造
注入替换 7 个公开 setter;真实循环依赖用 `ObjectProvider` 或 `@Lazy`
显式表达,而不是可变装配。镜像的测试文件在同一批切片中拆开。setter
调研(2026-10-31):7 个 setter 中 4 个是 Spring
`@Autowired(required = false)` 可选依赖注入(`lifecycle`、`definitions`、
`sessionWorktrees`、`journalEventTransport`),3 个是必需的普通
`@Autowired` 注入(`extensionPackages`、`creationSkills`、`worktreeFiles`
——后者的 bean 由 `ManagedAgentConfiguration.managedWorktreeFiles()`
供给;`VerificationFixtures` 里的直接调用是测试装配,不是生产接线)。
改为构造 `ObjectProvider` 注入是对这个 7,700 行中心类的大改动,涉及约
30 处测试装配点,且其正确性依赖 Spring 运行时装配——手动 setter 的单测
无法完全覆盖;应在实现前单独设计并做完整集成验证,而非作为增量切片。
管理器。模块 README 已允许共享连接的 SQL helper 留在同一包。构造注入
前置已完成(#16061):7 个公开 setter 已被单一 `@Autowired` 构造器取代,
持有 `final` 引用,可选协作者用 `@Nullable`、record-store 回环用 `@Lazy`,
镜像的 verification 夹具已迁移。setter 调研(2026-10-31,转换前):7 个
setter 中 4 个曾是 Spring `@Autowired(required = false)` 可选依赖注入
(`lifecycle`、`definitions`、`sessionWorktrees`、`journalEventTransport`),
3 个曾是必需的普通 `@Autowired` 注入(`extensionPackages`、`creationSkills`、
`worktreeFiles`——后者的 bean 曾由 `ManagedAgentConfiguration.managedWorktreeFiles()`
供给;`VerificationFixtures` 里的直接调用曾是测试装配,不是生产接线)。
[专门的构造装配设计](2026-11-01-managed-agent-store-wiring.zh-CN.md)
覆盖必需/可选依赖、额外的延迟 record-store 字段及其 Session-store 回环、
final 引用、直接夹具迁移和真实 Spring context 验证。该实现已在本地落地、
聚焦验证通过、完整 CI 验收待定;转换触及这个 7,657 行中心类与约 30 处
测试装配点。剩余工作是物理聚合拆分本身,每个聚合独立成切片。
- `RuntimeBrokerService`:为恢复扫描、取消、tool hold、observation 冷却
提取模块内协作者;服务本体退为门面。模块已经存在,不需要动任何跨模块
契约。这是风险最低、可读性收益最高的拆分。observation 冷却是第一条
@@ -215,24 +218,27 @@ sweep 管理这个陷阱。各字段的单位本身是一份被钉住的绑定
(`RuntimeHoldInventory`):持久化分页与 RELEASED 过滤逐字迁移,服务
保留准入、租户解析与错误映射边界。恢复扫描随后调研,证实深度耦合——
`scanExecutions` 驱动 `reconcileInContext`,后台进程 settle 族拥有
`SessionContext` 内存索引与 CAS settle 循环——现在提取会造出注入十来个
回调的协作者而非干净的缝;它等待对账路径先成为独立协作者。取消遵循
同一原则:只在缝干净处提取。带入对账切片的已知覆盖缺口:`lookupOnce`
中"先 stamp 后释放
在途槽位"的顺序没有测试钉住(base 上即存在的缺口);待对账路径提取
为独立协作者、该顺序成为缝级契约而非私有 lambda 内部时序时再钉。
- 对账、恢复扫描与取消**不是**干净的独立缝(2026-10-31 调研)。与冷却、
hold 清单(各自拥有自己的状态)不同,这些路径共享服务的核心可变状态
与生命周期机制:`sessions`、`liveBindings`、`reconciliations` 三个并发
map,绑定生命周期助手(`requireAnswerableBinding`、`requireLiveBinding`、
`invalidateBinding`、`provisioner.isUsable`),以及 CAS 执行 settle
(`absorbRuntimeStatus` → `settleUnstartedBackgroundSiblings`)。现在
提取其中任何一个,都会把十来个 map/助手/回调注入一个换个名字的上帝类
"协作者"。前置条件是一个**共享状态注册表**切片:把三个 map 与绑定生命
周期助手移到一个拥有它们的 `RuntimeBindingRegistry`(或类似)之后,
服务转为委托。注册表就位后,对账/恢复/取消依赖的是一个窄所有者而非
服务内部,从而可提取,M6 顺序也成为测试可钉的缝级契约。该注册表切片
比两条干净缝更大、风险更高,应在实现前单独设计与评审。
`SessionContext` 内存索引与 CAS settle 循环——独立提取会需要注入十来个
回调。该簇此后已通过 [Runtime Binding Registry](2026-10-31-runtime-binding-registry.zh-CN.md)
与[执行流程抽取](2026-11-01-runtime-execution-workflows.zh-CN.md)提取:
对账、恢复扫描与取消现分别落在 `RuntimeExecutionReconciler`、
`RuntimeExecutionRecovery`、`RuntimeExecutionCancellation`,簇内不再有
等待项。带入对账切片的覆盖缺口:base 中没有测试
固定 `lookupOnce` 先 stamp 后释放在途槽位的顺序。
[流程抽取](2026-11-01-runtime-execution-workflows.zh-CN.md) 现已在对账器边界
增加确定性阻塞 stamp 回归;其验证证据随该增量记录。
- 对账、恢复扫描与取消当时**不是**干净的独立边界(2026-10-31 调研)。前置条件是
[Runtime Binding Registry](2026-10-31-runtime-binding-registry.zh-CN.md)
首片:它持有 `liveBindings`、`bindingOperations`、`sessions`、本地
Session context 及 Binding 校验/失效 helper。源码审计修正了最初三张 map
的草案:`bindingOperations` 必须随 Binding 生命周期操作迁移;按 execution
ID 索引的 `reconciliations` 则先与 `ExecutionObservationCooldown` 放在
一起,已在后续对账切片迁出。Registry 首片中,服务仍保留供应、排空、恢复、
execution 结算、续租和 scheduler 生命周期。这样无需注入 service 回调就建立了具体持有者。
M6 先 stamp 后移除槽位的覆盖缺口由[执行流程抽取](2026-11-01-runtime-execution-workflows.zh-CN.md)
中阻塞 stamp 的回归处理;该切片在 Registry 后实现对账、接管扫描和取消。
Registry 首片本身不宣称具备该覆盖。并发、清理与验收要求见所链接的双语
设计;本地与 CI 验证分别记录。
- 之后对 `ManagedSessionStore` 与 `QwenHostedHarnessConnector` 做同样处理,
各自独立成切片。
@@ -0,0 +1,70 @@
# Managed Runtime backend modules
[English](2026-10-31-managed-runtime-backend-modules.md) | [简体中文](2026-10-31-managed-runtime-backend-modules.zh-CN.md)
Status: implemented; Java acceptance pending. Based on main `73474ef094ea1c6bb2a95a9a2a33a28b0ec58e9e`, including its Store constructor injection. Scope narrowed on 2026-11-02 to independently consumable Maven backends and their assembly. The existing shared local Java verification budget is exhausted at 599.048/600 seconds; earlier Local/External results do not certify the revised SPI and module graph.
## Objective and boundaries
After #15871, backend implementations still share the adapters artifact and server assembly directly constructs them. SSH additionally requires a Local provider. Split Local, SSH, Kubernetes, External and Static into independent Maven artifacts so another service can depend on the server and explicitly install only its chosen backend. Preserve the default full executable and operator distributions.
Backend availability comes from the application's trusted classpath; enabled selection comes from existing configuration. No live JAR loading, automatic download or untrusted-plugin isolation is introduced. Selective standalone builds, variant archives, deployment examples and their CI matrix are deferred. Kubernetes scratch, Session/Turn prewarm changes and further database/Harness/messaging dependency trimming are outside this change.
## Module boundaries
Modules remain reactor siblings with the same parent version and release process. Each production Java package has one artifact owner; package-private tests move with their classes.
| Module | Responsibility | Internal dependencies |
| -------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `contracts` | Runtime settings, identities and construction/access ports | None |
| `runtime-local` | Local Worker, persistent state, factory and offline recovery | `contracts`, `runtime-http`, `runtime-host` |
| `runtime-ssh` | SSH Worker, connections/tunnels and factory | `contracts`, `runtime-http`, `runtime-host` |
| `runtime-kubernetes` | Kubernetes client, resident/CSI provisioning, protection and reclamation | `contracts`, `runtime-http` |
| `runtime-external` | External Runtime client, scratch/workspace providers and factory | `contracts`, `runtime-http` |
| `runtime-static` | Preconfigured endpoint/lease and factory | `contracts` |
| `runtime-http` | Shared HTTP transport | `contracts` |
| `runtime-host` | Shared process/file exchange and host identity/state utilities | `contracts` |
| `runtime-broker` | Lease/execution coordination and backend-neutral routing | `contracts` |
| `adapters` | Harness, filesystem and other non-provider adapters | Shared modules; no backend implementation |
| `server` | Selection, SQL-backed wiring, HTTP and operator entry points | Application/shared modules; five optional runtime backends |
| `verification` | Tests and independent classpath/embedding fixtures; unpublished | All production modules |
The five backends never depend on one another. Routing belongs to `runtime-broker`; process/file exchange shared by Local and SSH belongs to `runtime-host`, which does not install a Local provider. SQL stores, reservation transactions, retirement authorization and shared CSI application-store wiring remain in their original layers. Kubernetes Harness discovery remains a separate Harness concern. New modules declare directly used libraries and participate in the existing compilation, architecture, Checkstyle, SpotBugs, source/Javadoc and aggregate coverage gates.
## Factory and dependency contracts
Each backend registers `RuntimeProviderFactory` through `META-INF/services`. The registry rejects duplicate/reserved kinds, checks the selected factory and verifies the returned provider's kind. Factory construction and discovery are inert; validation checks configuration only. Preflight keeps common listener, credential and Workspace checks, then delegates provider diagnostics. Remote providers do not require a local workspace directory. Existing Local executable/state checks and External workspace warnings remain.
Creation uses `create(settings, transport, dependencies)`. `RuntimeProviderDependencies` has exactly four named ports: `bindings()` for the original Broker repository, `externalStorage()` for the lazy SQL storage adapter, `csiReservations()` for the original datasource/transaction authority, and `ssh()` for borrowed node/storage lookup and connections. A backend requests only its required ports. Kubernetes retains JDBC repository, datasource identity, transaction manager and Workspace execution checks before reservation construction.
The discovered SSH factory also supplies connection construction through `SshRuntimeSupport`; the Kubernetes factory supplies operator/client/protection construction through `KubernetesRuntimeSupport` and the typed `WorkspaceCsiRuntimeSpec`. These are capabilities of the same factory, with no second discovery mechanism. The internal SPI and moved implementation imports require same-version recompilation. Persisted kinds, identity bytes, SQL schema, wire protocol and recovery algorithms do not change. Only `kubernetes-workspace` is registered; `kubernetes-scratch` remains unregistered and `kubernetes` remains rejected.
An additional trusted provider can implement the contract, register its factory and use `provider-options` without another server selection branch. It must preserve existing fencing, identity and HTTP contracts. Discovery does not establish whether a saved resource can be recovered; existing durable checks remain authoritative.
## Selection, admission and resource lifetime
`RuntimeBackendSelection` computes the enabled set before resource allocation. Disabled Broker means an empty set with no discovery or connections. SSH primary requires configured nodes and selects SSH alone. Local primary with nodes selects Local+SSH; any other primary with nodes is rejected. Otherwise only the primary backend is enabled. All selected factories and configuration are checked before clients, connections or Worker state are created.
SSH-only constructs no Local provider/state or routing wrapper. Local+SSH retains its shared endpoint resolver and original node identity, while the Local delegate uses its direct HTTP transport. SSH-only refuses unbound Session execution, Local workspace settings and historical Local path resolution. Static retains directory preflight; Kubernetes retains credentials, resident registration and physical-retirement authorization.
`ssh.enabled` still governs new registrations, bindings, Sessions and file access. When false, already-admitted work can execute, close, recover and perform permitted idempotent replay with its original node identity. When SSH is absent from the enabled set, historical registration queries remain available but do not claim or dispatch work, and parked acceptance stays unknown rather than becoming FAILED.
Online file-upload reconciliation checks the backend of the saved target before exchange or status dispatch. A historical Local upload after switching to SSH-only stays pending and retains its storage holder without executing the saved Local command. Restoring the backend resumes reconciliation. This guard does not disable independent offline storage maintenance.
Spring exclusively closes the shared SSH connections bean; providers and `SshWorkspaceService` borrow it, and the service closes only its executor. Failed startup and normal shutdown must release controller ownership. Factories close partially constructed resources; resident assembly closes earlier delegates if a later registration fails.
## Maven consumption and maintenance
The server declares five `optional` dependencies with `runtime` scope. Library consumers receive none transitively and explicitly add their chosen same-version backend artifacts. For example, Kubernetes requires the ordinary server and `qwen-managed-agent-runtime-kubernetes`; Local+SSH requires both corresponding artifacts. A disabled Broker needs no backend. Including several artifacts does not enable arbitrary composition or fallback.
The standalone build still includes all five backends. Keep the fixed `exec` classifier, existing operator classifiers and Central-release policy, application YAML/configuration metadata and Lombok exclusion. The ordinary library contains no standalone application YAML. Keep the reference packager and release names unchanged; no backend selector or variant metadata is added. Update only module-related Maven cache exclusions and their CI meta-tests.
Maintenance requests capabilities per action: missing Kubernetes rejects CSI `serve`, protection rendering and physical retirement, while CSI `text`/`retire` and SQL-only resident `inspect`/`begin` remain available. Local-dependent recovery requires Local before persistent side effects. Online validation belongs to normal configuration and explicit preflight, not shared properties initialization used by the SQL-only maintenance context. Missing capabilities never fall back to another backend.
## Verification and remaining risks
Keep independent compilation and bytecode dependency checks. Isolated classpaths cover absent backends, each single backend, Local+SSH, full and an independent contract-only provider; the ordinary Maven embedding fixture checks that no optional backend leaks transitively. Default executable packaging checks all five JARs and service registrations, configuration resources and operator launchers. These checks do not claim seven standalone variant products.
Retain regressions for unused-provider initialization, missing/duplicate factories before side effects, SSH admission, pending upload/registration recovery, failed-startup controller lock reacquisition and partial-construction cleanup. Preserve the existing full unit/H2, Checkstyle, SpotBugs, embedding, MySQL and Hosted/fault CI gates. Real enabled SSH/Kubernetes and complete cross-platform acceptance remain required; mocked connections or H2 do not establish that evidence.
New and revised Java tests await CI because the shared local budget is closed. Root script/workflow tests, formatting, link checks and static review cover the scope reduction; prior root build/typecheck/bundle evidence applies only while its inputs are unchanged. Detailed results remain in `.qwen/e2e-tests/runtime-provider-plugins.md`. Retain an owning backend until its work drains, or deliberately keep pending operations and storage claims until it is restored. No unresolved design decision blocks this scope.
@@ -0,0 +1,70 @@
# Managed Runtime 后端模块
[English](2026-10-31-managed-runtime-backend-modules.md) | [简体中文](2026-10-31-managed-runtime-backend-modules.zh-CN.md)
状态:已实现,Java 验收待完成。基于 main `73474ef094ea1c6bb2a95a9a2a33a28b0ec58e9e`,包含其中的 Store 构造注入改动。2026-11-02 将范围收敛为可独立引用的 Maven 后端及其装配。既有共享本地 Java 验证预算已消耗 599.048/600 秒;此前 Local/External 结果不能证明修订后的 SPI 和模块图正确。
## 目标与边界
#15871 之后,后端实现仍共用 adapters 产物,server 装配直接构造实现,SSH 还依赖 Local provider。将 Local、SSH、Kubernetes、External 和 Static 拆为独立 Maven 产物,使其他服务能引用 server 并显式安装所需后端。保留默认完整可执行包和运维分发。
后端是否可用由应用可信 classpath 决定,启用哪个后端由现有配置决定。不引入实时 JAR 加载、自动下载或不可信插件隔离。选择性独立构建、变体归档、部署示例及其 CI 矩阵后置。Kubernetes scratch、Session/Turn 预热调整,以及进一步裁剪数据库/Harness/消息依赖不在本次范围内。
## 模块边界
模块保持为同级 reactor 子模块,统一父版本和发布流程。每个生产 Java package 只有一个产物所有者;包私有测试随实现移动。
| 模块 | 职责 | 内部依赖 |
| -------------------- | ------------------------------------------------------ | ------------------------------------------- |
| `contracts` | Runtime 配置、身份及构造/访问接口 | 无 |
| `runtime-local` | 本地 Worker、持久状态、工厂及离线恢复 | `contracts`、`runtime-http`、`runtime-host` |
| `runtime-ssh` | SSH Worker、连接/隧道及工厂 | `contracts`、`runtime-http`、`runtime-host` |
| `runtime-kubernetes` | Kubernetes 客户端、常驻/CSI 供给、保护及回收 | `contracts`、`runtime-http` |
| `runtime-external` | 外部 Runtime 客户端、scratch/workspace provider 及工厂 | `contracts`、`runtime-http` |
| `runtime-static` | 预配置 endpoint/lease 及工厂 | `contracts` |
| `runtime-http` | 公共 HTTP transport | `contracts` |
| `runtime-host` | 共享进程/文件交换及主机身份/状态工具 | `contracts` |
| `runtime-broker` | lease/执行协调及与具体后端无关的路由 | `contracts` |
| `adapters` | Harness、文件系统及其他非 provider 适配器 | 共享模块;不含后端实现 |
| `server` | 选择、SQL 权威装配、HTTP 及运维入口 | 应用/共享模块;五个 optional runtime 后端 |
| `verification` | 测试及独立 classpath/嵌入夹具;不发布 | 全部生产模块 |
五个后端互不依赖。路由属于 `runtime-broker`;Local 与 SSH 共用的进程/文件交换属于 `runtime-host`,不会安装 Local provider。SQL 存储、预留事务、退休授权及共享 CSI application-store 装配保留在原层。Kubernetes Harness 发现仍是独立的 Harness 职责。新模块直接声明使用的库,并接入既有编译、架构、Checkstyle、SpotBugs、源码/Javadoc 及聚合覆盖率检查。
## 工厂与依赖契约
每个后端通过 `META-INF/services` 注册 `RuntimeProviderFactory`。注册表拒绝重复/保留 kind,检查选中工厂并核对返回 provider 的 kind。工厂构造与发现不分配资源;validation 只检查配置。预检保留公共监听、凭据及 Workspace 检查,再委派后端诊断。远程后端不要求本机 Workspace 目录。保留原 Local 可执行文件/状态检查和 External workspace 警告。
创建接口为 `create(settings, transport, dependencies)`。`RuntimeProviderDependencies` 只有四个具名接口:`bindings()` 提供原 Broker 仓储,`externalStorage()` 延迟提供 SQL 存储适配器,`csiReservations()` 提供原数据源/事务权威,`ssh()` 提供借用的节点/存储查询和连接。各后端只获取所需接口。Kubernetes 在构造预留存储前保留 JDBC 仓储、数据源身份、事务管理器及 Workspace 执行检查。
已发现的 SSH 工厂通过 `SshRuntimeSupport` 提供连接构造;Kubernetes 工厂通过 `KubernetesRuntimeSupport` 和类型明确的 `WorkspaceCsiRuntimeSpec` 提供运维/客户端/保护构造。这些是同一工厂的能力,不增加第二套发现机制。内部 SPI 和已移动的实现 import 需要按同版本重新编译。持久化 kind、身份字节、SQL schema、协议及恢复算法不变。只注册 `kubernetes-workspace`;`kubernetes-scratch` 仍未注册,`kubernetes` 仍被拒绝。
其他可信 provider 可以实现契约、注册工厂并使用 `provider-options`,无需增加 server 选择分支;它必须遵守既有 fencing、身份和 HTTP 契约。发现成功不能证明可恢复已有资源,原持久化检查仍具有决定权。
## 选择、准入与资源生命周期
`RuntimeBackendSelection` 在分配资源前计算启用集合。Broker 停用时集合为空,不发现工厂、不创建连接。SSH 主后端要求配置节点,只选择 SSH。Local 主后端配置节点时选择 Local+SSH;其他主后端与节点组合提前拒绝。其余情况只启用主后端。所有选中工厂及配置均在创建客户端、连接或 Worker 状态前检查。
SSH-only 不构造 Local provider/状态或路由包装。Local+SSH 保留共享 endpoint resolver 及原节点身份,Local delegate 使用直接 HTTP transport。SSH-only 拒绝未绑定 Session 的执行、Local workspace 设置和历史 Local 路径解析。Static 保留目录预检;Kubernetes 保留凭据、常驻注册及物理退休授权。
`ssh.enabled` 继续控制新注册、绑定、Session 和文件访问。设为 false 时,已接纳任务仍可按原节点身份执行、关闭、恢复及进行获准的幂等重放。SSH 不在启用集合时,历史注册仍可查询,但不 claim 或分发任务,暂停的 acceptance 保持 unknown,不改写为 FAILED。
在线文件上传对账在交换或状态分发前检查已保存目标所属的后端。切到 SSH-only 后,历史 Local 上传保持 pending 和存储占用,不执行已保存的 Local 命令;恢复后端后可继续对账。该限制不禁用独立离线存储维护。
Spring 唯一负责关闭共享 SSH connections bean;provider 与 `SshWorkspaceService` 仅借用,service 只关闭自己的 executor。启动失败与正常关闭均须释放 controller 所有权。工厂关闭部分构造的资源;常驻装配在后续注册失败时关闭此前 delegate。
## Maven 引用与运维
server 声明五个 `optional`、`runtime` 依赖。library 使用方不会传递引入任何后端,需要显式添加同版本的所选后端产物。例如,Kubernetes 使用方引用普通 server 和 `qwen-managed-agent-runtime-kubernetes`;Local+SSH 使用方引用对应的两个产物。Broker 停用时无需后端。安装多个产物不代表允许任意组合或回退。
独立应用构建仍包含五个后端。保留固定 `exec` classifier、现有运维 classifier 及 Central 发布策略、应用 YAML/配置元数据和 Lombok 排除。普通 library 不含独立应用 YAML。保持参考打包器及发布名称不变,不增加后端选择器或变体元数据。CI 仅更新模块相关的 Maven 缓存排除及配套元测试。
运维按 action 获取能力:缺少 Kubernetes 时拒绝 CSI `serve`、保护渲染和物理退休,但保留 CSI `text`/`retire` 及纯 SQL 的 resident `inspect`/`begin`。依赖 Local 的恢复在持久化副作用前要求 Local。在线校验属于正常配置及显式预检,不放入纯 SQL 维护上下文使用的共享 properties 初始化。缺少能力时不回退到其他后端。
## 验证与剩余风险
保留独立编译及字节码依赖检查。隔离 classpath 覆盖无后端、五种单后端、Local+SSH、full 及独立的 contract-only provider;普通 Maven 嵌入夹具检查 optional 后端不会传递泄漏。默认可执行包检查五个 JAR 及服务注册、配置资源和运维启动器。这些检查不宣称交付七种独立分发产品。
保留未选 provider 初始化、缺失/重复工厂在副作用前拒绝、SSH 准入、pending 上传/注册恢复、启动失败后 controller 锁重新获取及部分构造清理的回归。保留既有完整 unit/H2、Checkstyle、SpotBugs、embedding、MySQL 和 Hosted/fault CI 检查。真实启用的 SSH/Kubernetes 及完整跨平台验收仍是必需项;模拟连接或 H2 不能替代。
共享本地预算已关闭,新增及修订后的 Java 测试等待 CI。根目录脚本/工作流测试、格式、链接检查和静态审查覆盖此次收敛;仅当输入未变化时复用此前根目录 build/typecheck/bundle 证据。详细结果保留在 `.qwen/e2e-tests/runtime-provider-plugins.md`。移除后端前应排空其工作,或明确保留 pending 操作及存储占用直到原后端恢复。当前范围没有待用户决定的设计问题。
@@ -0,0 +1,86 @@
# Runtime Binding Registry
[English](2026-10-31-runtime-binding-registry.md) | [简体中文](2026-10-31-runtime-binding-registry.zh-CN.md)
- Status: First implementation slice; verification results are recorded separately. Subsequent workflow extraction is described in the [execution workflow design](2026-11-01-runtime-execution-workflows.md).
- Date: 2026-10-31
- Source baseline: `cf7e77810efa776f29b52a3dc4bf24f17d5e80f8`.
- Scope: module-internal extraction in `packages/managed-agent-server/runtime-broker`.
## Problem and current state
`RuntimeBrokerService` combines placement, acquire, execution, reconciliation, cancellation, recovery and shutdown in approximately 6,300 lines. Extracting reconciliation alone would require passing its mutable maps and numerous lifecycle callbacks into another class. The [structure review](2026-10-31-managed-agent-structure-review.md) therefore requires a state owner first.
The source audit refines that proposal: `bindingOperations` belongs with Binding state because provisioning, recovery, reclamation and drain all reserve the same Binding ID. `reconciliations` belongs with execution reconciliation because its execution-ID reservation must be released only after stamping `ExecutionObservationCooldown`. Moving these two maps in the opposite direction would retain the coupling we want to remove.
## Goals and boundaries
Establish one module-internal owner for local Binding and Runtime Session state, migrate all production consumers, and preserve observable behavior. This slice changes no public interface, HTTP route, schema, dependency, authentication rule or protocol byte. It does not change when `warm` runs; the separate on-demand Runtime proposal is outside this change.
Provisioning, attestation, drain, Session acquisition/release, execution settlement, recovery scans and renewal scheduling remain orchestrated by the service. They may use Registry operations, but the Registry never calls back into the service. Do not introduce an interface, generic operation runner, event bus or configurable lifecycle framework for this one owner.
## Ownership
| State or responsibility | Owner after this slice | Authority and consumers |
| ---------------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `liveBindings` | `RuntimeBindingRegistry` | Local attested generation/lease only; provisioning/adoption registers it; warm, recovery, reconciliation and extension controls consult it; drain, loss and invalidation remove it. |
| `bindingOperations` | Registry | One local reservation per Binding ID, shared by provisioning, reconciliation of a Binding, lost-generation reclamation, maintenance recovery and drain. It never replaces a repository operation claim. |
| `sessions` and `SessionContext` | Registry | Local Runtime Session acquisition/adoption futures and per-context lock, mount claim, active controls, background-process IDs and release futures. Acquire, execution/control, reconciliation, cancellation, release and drain use these contexts. |
| `reconciliations` and `ExecutionObservationCooldown` | Service, then a future reconciliation collaborator | Execution-ID deduplication and the existing stamp-before-reservation-removal ordering move together in a later slice. |
| `dispatches`, `invocations`, execution CAS and background-process settlement | Service | Dispatch and physical invocation tracking are not Binding ownership. |
| Operation/dispatch renewal, schedulers, `closed`, provisioner lifecycle | Service | The same close fence and renewal tasks remain in force. |
| Persisted Binding, Session, execution, admission and writer fencing | Repositories | Durable authority across Broker processes and restarts; Registry membership is never authorization or proof of stopped writers. |
`BindingContext` is an immutable record and lease pair used between orchestration steps. `SessionContext` retains its `ReentrantLock`: callers' compound decisions and the context's accessors use the same guard, including when repository calls block on JDK 21 virtual threads. The first slice preserves the background-process set's existing guarded use rather than changing settlement ownership at the same time.
## Registry operations
The Registry is package-private in the existing service package. It receives the Binding repository, provisioner, Broker owner ID, operation lease duration and clock directly. These are its concrete dependencies for the existing validation/invalidation operations; it has no scheduler and no service reference.
- Look up, reserve and conditionally remove Binding-operation futures. A caller keeps the future it installed; cleanup removes that exact reservation, so an old completion cannot drop a replacement.
- Look up, register and remove Session futures. Failed acquire/adoption removes its exact future. Successful release retains the existing removal points after durable release or an already-`RELEASED` read.
- Register or forget a locally attested Binding, match its generation and complete lease identity, and require that local route before use.
- Validate that an execution's original persisted generation remains `READY` or `DRAINING`. This read remains separate from local lease validation and from tenant, Harness and admission checks at their existing call sites.
- Invalidate an unusable Binding and reject unusable Session leases with the existing error codes.
- Cancel pending Binding and Session futures during service shutdown. The service continues to cancel dispatch and execution-reconciliation futures, stop both schedulers and close the provisioner.
Raw maps never escape. Futures and contexts are operation handles, not new durable state. The Registry does not start work, refresh authorization, acquire a replacement generation or manufacture a Session on a read path.
## Concurrency, leases and cleanup
Preserve these source-order contracts:
1. Provisioning/adoption persists readiness under the repository claim before registering the attested lease locally. Cached readiness must match generation, Runtime instance ID, endpoint, token, lease ID and epoch; a stale endpoint never becomes a fallback route.
2. Reserve a Binding ID with `putIfAbsent` before starting the corresponding local operation. Existing callers either join or receive the same operation-specific refusal as before. Conditional reservation removal precedes completion of its owned future, preserving synchronous continuation behavior.
3. Reserve a Session future before acquire. On failure, remove that future before completing it exceptionally. Existing Session identity/admission checks, mount-claim replacement, control counters and release guards retain their locations and lock boundaries.
4. Invalidation first revokes the local route, then attempts a repository operation claim and the existing `READY` to `LOST` CAS. A failed claim or write must not restore the route. Release the claim in `finally`, using the exact Broker owner and operation generation; a failed best-effort release expires naturally. Invalidation alone proves neither loss evidence nor stopped writers and does not release Sessions or physical resources.
5. Physical Session release still follows durable `RELEASING` admission, worker acknowledgement and durable release completion. Failed/negative release resets the existing latch for retry. No local cache move bypasses active-execution or background-hold checks.
6. Service close sets the existing atomic fence before canceling Registry futures, then cancels the other operation futures, shuts down schedulers and closes the provisioner. Registry cancellation introduces no separate closed flag, teardown order or physical cleanup policy.
Binding IDs name durable physical generations; repository operation generations independently fence successive owners of a claim. The extraction preserves both identities and all repository checks. Process-local futures only coalesce work inside one Broker and cannot establish cross-process exclusivity.
## Implementation and follow-up sequence
The first slice adds `RuntimeBindingRegistry`, moves its maps, local context types and Binding validation/invalidation helpers, and migrates every service call site. Test-only context installation follows the new owner through `BrokerServiceTestAccess`; production gains no test hook. Update both versions of the structure review to reference the corrected ownership boundary.
The [subsequent workflow design and implementation](2026-11-01-runtime-execution-workflows.md) follows this sequence:
1. Execution reconciliation, taking its execution-ID reservations and cooldown ordering together. Pin stamp-before-reservation-removal at that seam; this Registry slice does not claim to close the existing coverage gap; the subsequent extraction adds the blocked-stamp regression.
2. Recovery scanning and its background-process settlement dependencies, after the reconciliation seam exists.
3. Cancellation and its original-generation routing and durable intent handling.
Each follow-up has its own diff and acceptance evidence. Moving all workflows into this Registry would merely recreate the large service under a new name.
## Validation and acceptance
The change is an internal refactor, so use the existing Java service and HTTP regressions instead of inventing a global CLI baseline or a new behavior E2E plan. Run focused Registry tests for full lease matching, persistent-generation eligibility, invalidation order and claim cleanup, reservation replacement and shutdown. Existing service tests cover concurrent warm/acquire/release, changed leases, stale provisioning completion, claim renewal, reconciliation routing and close fencing. Include the affected HTTP tests whose context fixtures move, durable recovery tests, the virtual-thread guard regression and module architecture checks within the module's shared local verification budget.
Preflight JDK 21, Maven >= 3.8.9 and a source-matching SDK in the task's isolated Maven repository. Use the module's focused Maven test/Checkstyle command and record the exact selected classes, source state, results and omissions. Full Java/SpotBugs, MySQL and Hosted/fault gates remain CI work. Selected in-memory or H2 cases do not establish MySQL, real-process recovery or production acceptance. If the relevant real-process path cannot run locally within budget, record it as pending rather than replacing it with a fixture claim.
Acceptance requires no raw Registry maps or context definitions in the service, no service dependency or injected callbacks in the Registry, unchanged external contracts and repository algorithms, meaningful focused regression evidence, two clean self-audit passes, synchronized bilingual documents and the required CI gates on the accepted revision. Local completion of the extraction alone does not mean CI acceptance.
## Risks and resolved questions
The main risks are changed completion order, accidental weakening of local lease checks, altered exception classification and loss of the shared context guard. Mitigate them with mechanical moves, caller-by-caller comparison and the focused tests above. Keep existing unconditional Session removal and Binding retirement points; changing their policy would be a separate behavior change.
Resolved: Registry owns Binding-operation reservations, not execution reconciliation reservations. The service owns renewal and shutdown. Durable admission remains at the original repository call sites. No open product decision is required for this slice; the subsequent collaborator APIs and validation are specified in the linked workflow design.
@@ -0,0 +1,86 @@
# Runtime Binding Registry
[English](2026-10-31-runtime-binding-registry.md) | [简体中文](2026-10-31-runtime-binding-registry.zh-CN.md)
- 状态:首个实现切片;验证结果单独记录。后续流程抽取见[执行流程设计](2026-11-01-runtime-execution-workflows.zh-CN.md)。
- 日期:2026-10-31
- 源码基线:`cf7e77810efa776f29b52a3dc4bf24f17d5e80f8`。
- 范围:`packages/managed-agent-server/runtime-broker` 模块内部抽取。
## 问题与当前状态
`RuntimeBrokerService` 在约 6,300 行中混合了放置、acquire、执行、对账、取消、恢复和关闭。单独抽取对账需要向新类传递可变 map 和大量生命周期回调。因此,[结构评审](2026-10-31-managed-agent-structure-review.zh-CN.md) 要求先明确状态持有者。
源码审计修正了原提案:`bindingOperations` 应与 Binding 状态归于同一持有者,因为供应、恢复、回收和排空都预留同一个 Binding ID。`reconciliations` 则属于执行对账,因为必须先写入 `ExecutionObservationCooldown`,再释放按 execution ID 预留的槽位。反向迁移这两张 map 会保留本次要解除的耦合。
## 目标与边界
为本地 Binding 和 Runtime Session 状态建立唯一的模块内部持有者,迁移全部生产调用方,并保持可观察行为。首片不改变公共接口、HTTP 路由、schema、依赖、认证规则或协议字节。不改变 `warm` 的调用时机;按需启动 Runtime 的独立提案不在本次范围内。
供应、身份核验、排空、Session 获取/释放、执行结算、恢复扫描和续租调度仍由服务编排。它们可以调用 Registry,但 Registry 不得回调服务。不为这一个持有者引入接口、通用操作运行器、事件总线或可配置生命周期框架。
## 状态归属
| 状态或职责 | 首片后的持有者 | 权威来源与调用方 |
| --------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `liveBindings` | `RuntimeBindingRegistry` | 仅表示本地已核验的代际/租约;供应和接管登记;warm、恢复、对账和扩展控制读取;排空、丢失和失效处理移除。 |
| `bindingOperations` | Registry | 按 Binding ID 预留本地操作,由供应、Binding 对账、丢失代际回收、维护恢复和排空共享;不替代 repository 操作租约。 |
| `sessions` 与 `SessionContext` | Registry | 本地 Runtime Session 获取/接管 future,以及每个 context 的锁、mount claim、活跃 control、后台进程 ID 和 release future。acquire、执行/control、对账、取消、释放和排空使用这些 context。 |
| `reconciliations` 与 `ExecutionObservationCooldown` | 当前服务,后续对账协作者 | 按 execution ID 去重,以及先 stamp 后移除预留槽位的顺序,在后续切片中一起迁移。 |
| `dispatches`、`invocations`、execution CAS 和后台进程结算 | 服务 | 分发与物理调用跟踪不属于 Binding 状态归属。 |
| 操作/分发续租、scheduler、`closed`、provisioner 生命周期 | 服务 | 保留同一关闭屏障与续租任务。 |
| 持久化 Binding、Session、execution、准入和 writer fencing | Repository | 跨 Broker 进程与重启的持久权威;Registry 成员资格绝不是授权或 writer 已停止的证明。 |
`BindingContext` 是编排步骤间传递的不可变 record 与 lease 对。`SessionContext` 保留 `ReentrantLock`:调用方的复合决策与 context 访问器使用同一把锁,包括 repository 调用在 JDK 21 虚拟线程上阻塞的场景。首片保持后台进程集合现有的加锁使用方式,不同时修改结算归属。
## Registry 操作
Registry 是现有 service package 中的 package-private 类。直接接收 Binding repository、provisioner、Broker owner ID、operation lease duration 和 clock。这些是现有校验/失效操作的具体依赖;它没有 scheduler 或 service 引用。
- 查找、预留并有条件地移除 Binding 操作 future。调用方保留自己安装的 future;清理只移除该预留项,旧操作完成不能删除替代项。
- 查找、登记和移除 Session future。acquire/接管失败时只移除自己的 future。成功释放保留持久化释放完成或读到已 `RELEASED` 后的现有移除位置。
- 登记或移除本地已核验的 Binding,匹配代际及完整租约身份,并在使用前要求这条本地路由存在。
- 验证 execution 的原持久化代际仍为 `READY` 或 `DRAINING`。该读取继续独立于本地租约校验,以及原位置的 tenant、Harness 与准入检查。
- 将不可用 Binding 标为失效,并按现有错误码拒绝不可用的 Session lease。
- 服务关闭时取消未完成的 Binding 与 Session future。服务继续取消分发与执行对账 future、停止两个 scheduler 并关闭 provisioner。
不暴露原始 map。future 和 context 是操作句柄,不是新的持久状态。Registry 不启动工作、不刷新授权、不获取替代代际,也不在读取路径上制造 Session。
## 并发、租约与清理
保留下列源码顺序契约:
1. 供应/接管在 repository claim 下持久化就绪后,才在本地登记核验后的 lease。缓存就绪必须匹配代际、Runtime instance ID、endpoint、token、lease ID 和 epoch;过期 endpoint 不能成为回退路由。
2. 用 `putIfAbsent` 预留 Binding ID 后才启动相应本地操作。现有调用方继续 join 或收到原有的操作特定拒绝。有条件移除预留项先于完成其持有的 future,保持同步 continuation 行为。
3. acquire 前先预留 Session future。失败时先移除该 future,再异常完成。现有 Session 身份/准入检查、mount claim 替换、control 计数和 release guard 保留原位置与锁边界。
4. 失效处理先撤销本地路由,再尝试 repository 操作 claim 与现有 `READY` 到 `LOST` 的 CAS。claim 或写入失败不得恢复路由。在 `finally` 中以准确的 Broker owner 和 operation generation 释放 claim;尽力释放失败时由租约自然过期。失效本身不构成丢失证据或 writer 停止证明,也不释放 Session 或物理资源。
5. 物理 Session 释放仍按持久化 `RELEASING` 准入、worker 确认、持久化释放完成的顺序执行。失败/否定回复重置现有 latch 以便重试。本地缓存迁移不得绕过活跃 execution 或后台 hold 检查。
6. 服务 close 先设置现有原子屏障,再取消 Registry future,随后取消其他操作 future、关闭 scheduler 和 provisioner。Registry 取消操作不引入独立 closed 标志、拆除顺序或物理清理策略。
Binding ID 标识持久的物理代际;repository operation generation 独立隔离同一 claim 的历次 owner。抽取保留两种身份和全部 repository 检查。进程内 future 仅合并同一 Broker 中的工作,不能建立跨进程互斥。
## 实现与后续顺序
首片新增 `RuntimeBindingRegistry`,迁移其 map、本地 context 类型及 Binding 校验/失效 helper,并迁移全部 service 调用点。测试通过 `BrokerServiceTestAccess` 按新持有者安装 context;生产代码不新增测试入口。同时更新两个语言版本的结构评审,引用修正后的归属边界。
[后续流程设计与实现](2026-11-01-runtime-execution-workflows.zh-CN.md) 遵循以下顺序:
1. 执行对账,同时迁移按 execution ID 预留的槽位和 cooldown 顺序。在该边界固定先 stamp 后移除预留项的测试;Registry 首片不宣称补齐现有覆盖缺口;后续抽取增加阻塞 stamp 的回归。
2. 对账边界具备之后,再拆恢复扫描及其后台进程结算依赖。
3. 取消执行及其原代际路由和持久取消意图处理。
每项后续工作独立提供 diff 和验收证据。把所有流程搬进 Registry,只会换一个名字重新形成大服务。
## 验证与验收
本次是内部重构,复用现有 Java service 与 HTTP 回归,不构造全局 CLI 基线或新的行为 E2E 计划。定向 Registry 测试覆盖完整 lease 匹配、持久代际资格、失效顺序与 claim 清理、预留项替换和关闭。现有 service 测试覆盖并发 warm/acquire/release、租约变化、过期供应完成、claim 续租、对账路由和关闭屏障。在模块共享本地验证预算内,包括 context 夹具迁移所影响的 HTTP 测试、持久恢复测试、虚拟线程 guard 回归和模块架构检查。
预检 JDK 21、Maven >= 3.8.9,以及任务隔离 Maven 仓库中匹配源码的 SDK。按模块要求运行定向 Maven test/Checkstyle,记录准确的测试类、源码状态、结果和遗漏。完整 Java/SpotBugs、MySQL 和 Hosted/fault 门禁交由 CI。选定的内存或 H2 用例不构成 MySQL、真实进程恢复或生产验收。相关真实进程路径若无法在本地预算内运行,记录为待验证,不以 fixture 结果替代。
验收要求:service 不再持有 Registry 的原始 map 或 context 定义;Registry 不依赖 service 或注入回调;外部契约和 repository 算法保持不变;具备有意义的定向回归证据;完成两轮干净自审;双语文档同步;被验收版本通过必要 CI 门禁。本地抽取完成不表示 CI 已验收。
## 风险与已决问题
主要风险是完成顺序变化、本地 lease 校验被意外削弱、异常分类改变,以及丢失共享 context guard。通过机械迁移、逐调用方对比和上述定向测试控制风险。保留现有无条件移除 Session 和退役 Binding 的位置;更改该策略属于独立行为变更。
已决:Registry 持有 Binding 操作预留项,不持有执行对账预留项。续租与关闭归服务。持久准入保留在原 repository 调用位置。首片没有待定产品决策;后续协作者 API 和验证已在所链接的流程设计中明确。
@@ -0,0 +1,50 @@
# Managed Agent Store Constructor Wiring
[English](2026-11-01-managed-agent-store-wiring.md) | [简体中文](2026-11-01-managed-agent-store-wiring.zh-CN.md)
- Status: Implemented locally; focused verification passed, full CI qualification remains pending.
- Date: 2026-11-01
- Baseline: `06c1f989715fa812fc26258525ef97627c15b855`.
- Scope: `ManagedAgentStore` dependency assembly and its direct verification fixtures, independently of PR #16029.
## Problem and current state
The [structure review](2026-10-31-managed-agent-structure-review.md) identifies seven public setters on the Session store. Three are required Spring dependencies (`WorktreeFiles`, `ManagedSessionSkills`, `ExtensionPackages`); four are optional (`WorkspaceLifecycleStore`, `ManagedAgentDefinitionStore`, `SessionWorktreeStore`, `JournalEventTransport`). The constructor also creates a standalone skills store which Spring subsequently replaces. The object can therefore exist before its dependency assignment is complete.
There is an eighth injected dependency: optional, lazy `ManagedExtensionRecordStore` field injection. It has a real cycle back through `AgentStateStore`. The skills store also has its own existing lazy resource-store dependency. The other immediate dependency constructors have no eager edge back to the Session store: lifecycle uses providers for runtime resources, definitions is JDBC/digests, worktrees uses JDBC/registry/storage guard, extension packages uses its catalog/registry/archive dependencies, and journal transport uses its messaging dependencies.
## Goals and boundaries
Make all eight dependency references final, assign them through construction and remove all seven setters and the injected field. Preserve required/optional Spring resolution, the lazy record-store cycle boundary, qualifiers, transactions, SQL, protocol bytes and feature-disabled behavior. Do not change adjacent stores' injection, split business logic, change schema/routes or introduce a dependency container, builder or generic provider abstraction in production.
## Construction design
Use one explicitly `@Autowired` constructor containing the existing seven inputs, the three required dependencies and the five optional dependencies. Required dependencies remain ordinary constructor parameters so missing Spring beans still fail startup. Optional dependencies use `@Nullable` and retain the existing null guards in business methods. Inject the record store as `@Lazy @Nullable`: the lazy proxy remains unresolved during construction, preserving the original lazy field behavior and avoiding eager resolution of its `AgentStateStore` back-reference. Do not resolve that proxy or call a dependency from the constructor.
The constructor stores the injected skills instance directly; it no longer manufactures a temporary replacement. Preserve the existing qualified JDBC, mapper and clock parameters and every settings-derived field. The optional definitions, lifecycle, worktree and transport references remain concrete objects, avoiding providers whose repeated resolution could change the current null checks or dependency identity.
Keep the existing seven-argument standalone constructor because two production private-CSI helpers construct a minimal store outside Spring. It delegates once to the complete constructor, supplies the same standalone skills implementation and leaves integrations absent as before. Spring selects only the annotated complete constructor. This is an existing concrete use case, not a new compatibility ladder: no setter bridge or further constructor overload is added.
## Consumers and test fixtures
Production component scanning continues to construct the store; the private-CSI helpers retain their existing standalone construction. The test fixture that previously added local worktree files after construction becomes a constructor factory. Its default form constructs the same standalone skills behavior and local worktree adapter. A second test-only form accepts the definitions and lifecycle objects actually configured by existing tests; those callers now provide them before construction. Keep the same transactional wrapper, settings, JDBC instance and test assertions.
Migrate every `VerificationFixtures.configure(new ManagedAgentStore(...))` call to that factory. Bare standalone constructors used by read-only/private fixtures remain valid. Move the few definitions/lifecycle setup statements before factory construction; when a lifecycle fixture uses a result-store provider back-reference, register the result store after constructing the Session store, as required by that existing provider graph. Do not mutate final fields reflectively or rebuild a constructed store to emulate a setter.
Spring `@Bean` factories are a separate construction path: Spring previously applied the removed setters after receiving the returned object, but it does not rerun the annotated constructor on that object. The close, retention and action test factories must therefore receive all collaborators through their method parameters and invoke the complete constructor directly, preserving required parameters, nullable optional parameters and the lazy record-store parameter. Keep their existing properties, clock and publisher choices. The standalone test factory is only for manually assembled fixtures and must not substitute for Spring assembly.
## Validation
Add real Spring context tests with circular references disabled. Cover all required dependencies present, each required dependency missing, optional dependencies absent, the actual record-store/Session-store cycle, lazy target resolution and transaction-proxy identity. Assert the store has no injected fields or dependency-mutating methods and that all eight dependency fields are final. Required-bean checks must exercise Spring resolution, not just constructor annotations.
Load each of the three actual test factory configurations in a Spring context and verify its injected collaborator identities, deferred record-store initialization and back-reference to the completed transaction proxy. Run the existing close, retention and action scenarios as behavioral regression coverage, including the retention guard cases affected by uncaught background cleanup failures in the original CI run.
Reuse the application-context admission and skills tests, definition/approval tests and lifecycle fixtures affected by setup migration. Include a representative existing complete application context so the real component assembly is exercised alongside the minimal context. Keep SQL/repository algorithms unchanged and compare method bodies outside the constructors and removed setters. Run focused Java tests and Checkstyle under the module's shared 10-minute local verification budget using JDK 21 and a source-matching SDK; full Java/SpotBugs and platform/integration qualification remain CI work. This internal assembly refactor needs no global CLI baseline or TypeScript build.
Documentation verification checks both complete language versions, relative links and formatting. Self-audit the complete diff, including mechanically migrated fixtures, until two consecutive clean passes. Record selected tests and unrun gates separately; local context/H2 passes do not establish MySQL, Hosted or production acceptance.
## Risks, acceptance and open questions
The principal risks are making an optional bean required, eagerly resolving the record-store cycle, injecting a raw early reference instead of the transactional proxy, or changing manual fixture behavior. Preserve the lazy annotation rather than replacing it with eager `getIfAvailable`, and pin the runtime bean graph in tests. The standalone constructor's deliberately absent integrations are unchanged; the complete Spring constructor still requires its three beans.
Acceptance requires all seven public setters and field injection gone, eight final dependency fields, no change to business method bodies or SQL, migrated direct callers, successful required/optional/cycle/proxy context tests and affected regression tests, complete synchronized bilingual designs and two clean self-audit passes. No product decision is open; full CI acceptance remains pending until its required gates run on the accepted revision.
@@ -0,0 +1,50 @@
# Managed Agent Store 构造装配收敛
[English](2026-11-01-managed-agent-store-wiring.md) | [简体中文](2026-11-01-managed-agent-store-wiring.zh-CN.md)
- 状态:本地已实现,定向验证通过;完整 CI 验收仍待完成。
- 日期:2026-11-01
- 基线:`06c1f989715fa812fc26258525ef97627c15b855`。
- 范围:`ManagedAgentStore` 依赖装配及直接验证夹具,独立于 PR #16029。
## 问题与当前状态
[结构评审](2026-10-31-managed-agent-structure-review.zh-CN.md) 指出了 Session store 的七个公开 setter。其中三个是 Spring 必需依赖(`WorktreeFiles`、`ManagedSessionSkills`、`ExtensionPackages`),四个可选(`WorkspaceLifecycleStore`、`ManagedAgentDefinitionStore`、`SessionWorktreeStore`、`JournalEventTransport`)。构造器还会创建一个 standalone skills store,随后由 Spring 替换。因此,对象可以在依赖尚未装配完整时存在。
另有第八处注入:可选且延迟的 `ManagedExtensionRecordStore` 字段注入。它经 `AgentStateStore` 存在真实回环。skills store 自身也有现存的延迟 resource-store 依赖。其余直接依赖的构造器不立即回调 Session store:lifecycle 经 provider 获取 runtime 资源,definitions 依赖 JDBC/digests,worktrees 依赖 JDBC/registry/storage guard,extension packages 依赖 catalog/registry/archive,journal transport 依赖消息组件。
## 目标与边界
将八个依赖引用全部改为 final,经构造赋值,移除七个 setter 与注入字段。保留 Spring 必需/可选解析、延迟 record-store 回环边界、qualifier、事务、SQL、协议字节和功能关闭行为。不修改相邻 store 的注入,不拆业务逻辑,不改 schema/路由,也不在生产代码引入依赖容器、builder 或通用 provider 抽象。
## 构造设计
使用一个显式 `@Autowired` 构造器,包含现有七个输入、三个必需依赖和五个可选依赖。必需依赖使用普通构造参数,缺少 Spring bean 时继续启动失败。可选依赖使用 `@Nullable`,业务方法保留原空值判断。record store 使用 `@Lazy @Nullable` 注入:构造期间不解析延迟代理,保留原延迟字段行为,避免立即解析其 `AgentStateStore` 回引。构造器不解析该代理,也不调用依赖。
构造器直接保存注入的 skills 实例,不再制造随后被替换的临时对象。保留现有 JDBC、mapper、clock 的 qualifier,以及全部根据 settings 推导的字段。可选 definitions、lifecycle、worktree 和 transport 仍是具体对象,避免 provider 的重复解析改变当前空值判断或依赖身份。
保留已有七参数 standalone 构造器,因为两个生产 private-CSI helper 在 Spring 外构造最小 store。它只委托一次到完整构造器,提供同样的 standalone skills 实现,并像以前一样不配置集成依赖。Spring 只选择带注解的完整构造器。这是已有的具体用途,不新增兼容重载链:不保留 setter 桥,也不继续添加构造器重载。
## 调用方与测试夹具
生产环境继续由组件扫描构造 store;private-CSI helper 保留已有 standalone 构造。原先在构造后补 local worktree files 的测试夹具改为构造工厂。默认形式构造相同的 standalone skills 行为和本地 worktree adapter。第二种仅测试形式接收已有测试实际配置的 definitions 与 lifecycle 对象,这些调用方改为构造前提供。保留相同的事务包络、settings、JDBC 实例和测试断言。
将所有 `VerificationFixtures.configure(new ManagedAgentStore(...))` 调用迁到该工厂。只读/private 夹具使用的裸 standalone 构造继续有效。少量 definitions/lifecycle 配置语句移到工厂构造前;若 lifecycle 夹具通过 result-store provider 回引,则按原 provider 图在 Session store 构造后登记 result store。不通过反射修改 final 字段,不重建已构造 store 来模拟 setter。
Spring `@Bean` 工厂是另一条构造路径:以前 Spring 会对工厂返回的对象继续执行已移除的 setter,但不会对该对象重新执行带注解的构造器。因此,close、retention 和 action 测试工厂必须通过方法参数接收全部协作者,直接调用完整构造器,保留必需参数、可空的可选参数和延迟 record-store 参数。各工厂原有的 properties、clock 和 publisher 选择保持不变。standalone 测试工厂只用于手工装配的夹具,不能代替 Spring 装配。
## 验证
新增禁用循环引用的真实 Spring context 测试。覆盖必需依赖全部存在、分别缺少各必需依赖、可选依赖缺席、实际 record-store/Session-store 回环、延迟 target 解析及事务代理身份。断言 store 没有注入字段或修改依赖的方法,八个依赖字段全部 final。必需 bean 检查必须验证 Spring 解析行为,不能只检查构造器注解。
在 Spring context 中逐一加载三个实际测试工厂配置,验证注入协作者的身份、record store 的延迟初始化,以及其回引的是完成装配后的事务代理。运行已有 close、retention 和 action 场景作为行为回归覆盖,并包含原 CI 中被后台清理未捕获异常影响的 retention guard 用例。
复用应用 context 准入与 skills 测试、definition/approval 测试及装配迁移影响的 lifecycle 夹具。包括一个有代表性的现有完整应用 context,以便最小 context 之外也覆盖真实组件装配。SQL/repository 算法不变,对比构造器和已删除 setter 之外的方法体。在模块共享的 10 分钟本地验证预算内,以 JDK 21 和匹配源码的 SDK 运行定向 Java 测试与 Checkstyle;完整 Java/SpotBugs 及平台/集成验收交给 CI。该内部装配重构不需要全局 CLI 基线或 TypeScript 构建。
文档验证覆盖两个完整语言版本、相对链接和格式。对包含机械迁移夹具的完整 diff 进行自审,直到连续两轮干净。分别记录选中测试与未运行门禁;本地 context/H2 通过不构成 MySQL、Hosted 或生产验收。
## 风险、验收与待定问题
主要风险是把可选 bean 变成必需、立即解析 record-store 回环、注入原始早期引用而非事务代理,或改变手动夹具行为。保留延迟注解,不用立即 `getIfAvailable` 替换,并用测试固定运行时 bean 图。standalone 构造器刻意缺席的集成依赖保持不变;完整 Spring 构造器继续要求其三个 bean。
验收要求:移除七个公开 setter 与字段注入;八个依赖字段 final;业务方法体和 SQL 不变;直接调用方迁移完成;必需/可选/回环/代理 context 测试和受影响回归通过;双语设计完整同步;两轮自审干净。没有待定产品决策;在目标修订通过所需门禁前,完整 CI 验收仍待完成。
@@ -0,0 +1,76 @@
# Runtime execution dispatcher ownership
[English](2026-11-01-runtime-execution-dispatcher.md) | [简体中文](2026-11-01-runtime-execution-dispatcher.zh-CN.md)
- Status: implemented locally; focused verification passed; full CI acceptance pending.
- Date: 2026-11-01
- Baseline: `2aa908dc70785e38299a43964e064dbafaf7b878`, after #16029.
## Problem and current state
The execution workflows have explicit owners, but `RuntimeBrokerService` still owns dispatch reservations, physical invocations, dispatch-lease renewal and completion. Cancellation receives the invocation set and a per-call service callback. Moving just the transport call would leave the state and cleanup decisions split between owners.
This increment extracts one package-private `RuntimeExecutionDispatcher` inside the existing runtime-broker module. It preserves the current algorithms, admission boundaries, synchronous exceptions, asynchronous completion and shutdown order. It introduces no public API, HTTP route, SQL/schema, configuration, dependency or product behavior change.
The earlier full-tier run for #16029 contains an unresolved `WorkspaceCsiHostedFilesTest` cold-recovery failure before its MySQL integration phase. Preserve that evidence separately. Developing this extraction does not establish that the baseline failure is fixed or that the merged refactor has complete integration acceptance.
## Ownership and consumers
| Owner | Responsibility | Direct consumers |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `RuntimeExecutionDispatcher` | Dispatch reservations, physical invocation membership, claim-to-EXECUTING transition, dispatch renewal, transport invocation, v3 result waiting and persisted SSH acknowledgement | Service execution/start and drain paths; cancellation |
| `RuntimeExecutionState` | Existing durable execution ownership, fenced settlement, UNKNOWN conversion and background-sibling settlement | Dispatcher and existing execution workflows |
| `RuntimeExecutionCancellation` | Durable cancellation intent, physical cancellation and prepared-provider confirmation | Service cancellation facade |
| `RuntimeBindingRegistry` | Existing local Session contexts, route identity and Binding invalidation | Service and execution collaborators |
| `RuntimeBrokerSupport` | Existing shared rules plus unchanged detached/no-start receipt projections and renewal-delay calculation | Service and dispatcher |
| `RuntimeBrokerService` | Public admission and payload/grant validation, provisioning, Session/drain/stop orchestration, shared close flag, both executors and provisioner lifecycle | Existing adapters and assembly |
The dispatcher borrows the existing repositories, Registry, execution state, provisioner, transport, publication verifier, clock, lease/window settings, executors and close flag. Dependencies remain explicit constructor arguments; no general context container, interface or Spring bean is added.
The complete direct-consumer audit is the service's creation and two start families, its drain acknowledgement sweep and close sequence, cancellation's two invocation checks and unsent-dispatch branch, and `BrokerServiceTestAccess.pollDelay`. Other service receipt paths retain the shared `sshNoStartReceipt` and `isDetachedCapture` projections through Support. Repository and transport contracts remain unchanged.
## Dispatch admission and execution
Move all three `beginDispatch` overloads, `dispatch`, `enterExecuting`, reference/capture projection and `DispatchRenewal` together. Private CSI restrictions, Harness admission and deferred-payload refusal stay in their original order before reservation acquisition. Existing Session-context lock regions remain at callers; the dispatcher adds no lock around admission or dispatch.
The reservation map uses `putIfAbsent` and conditional `remove(executionId, exactFuture)` on synchronous failure and asynchronous completion. It covers claim/fencing work as well as physical invocation. The distinct invocation set starts only immediately before calling transport, after renewal starts. Cancellation observes that set through `isInvoking(executionId)` rather than receiving mutable state.
Preserve claim, fenced EXECUTING transition, owner/generation checks, usable-lease validation, renewal start and transport invocation in that order. A cancelled unsent execution still settles through the existing dispatcher path. Do not replace it with a new cancellation settlement algorithm or replay UNKNOWN work.
On completion, remove the physical invocation marker before persisting either the settled result or UNKNOWN outcome. Preserve background-sibling settlement and best-effort SSH acknowledgement after the durable write. A transport/null-result or ordinary settlement failure retains the existing UNKNOWN mapping. Synchronous `RuntimeBrokerException` values remain unchanged; other synchronous runtime/JVM failures retain `runtime_execution_dispatch_failed` wrapping and reservation cleanup.
## V3 results and SSH acknowledgement
V3 result waiting is part of the dispatch call closure and moves unchanged. Keep publication-first evidence reads, dispatch/Binding identity checks, lease/deadline refusal, explicit `not_started`, detached results, non-retryable error handling and 100 ms doubling backoff capped at 2 s. The test-only backoff accessor points to the new owner.
Move `acknowledgePersistedTools` into the dispatcher because dispatch completion and the service's drain sweep both consume it. Drain and physical Runtime stop remain service orchestration. Move the shared stateless `sshNoStartReceipt`, `isDetachedCapture` and `renewalDelayMillis` helpers to existing Support; do not duplicate their policies or introduce another collaborator.
## Cancellation and shutdown
Cancellation receives the concrete dispatcher. Replace the raw invocation-set reads with `isInvoking` and the `BiConsumer` callback with `beginDispatch(context, requested)`. Keep ownership reads, durable intent, admission refusals, control guards and physical cancellation order unchanged.
The service remains the sole writer of `closed` and the owner of both executors and the provisioner. Its shutdown sequence stays: set the close fence; cancel Registry operations; cancel dispatcher reservation futures; cancel reconciler operations; shut down coordination and renewal executors; close the provisioner.
`cancelPendingOperations` cancels only the existing reservation-wrapper futures. It does not cancel underlying transport stages, clear physical invocation markers or shut down borrowed resources. Do not add a new close check inside `beginDispatch`; changing the handling of already-admitted operations is outside this extraction.
## Files and migration
Add `RuntimeExecutionDispatcher`; update service construction/delegation, cancellation, shared Support and the test-only backoff accessor. Add focused dispatcher tests and retain the existing service, HTTP, SSH and protocol regressions. Link this design from both execution-workflow documents and update their current ownership description together.
Implement after the baseline dry-run: move state and the complete dispatch closure; wire the dispatcher before cancellation; replace all direct consumers; update tests and both design languages. Keep migration mechanical and review the complete diff for accidental algorithm or exception-boundary changes.
## Verification and acceptance
Follow `packages/managed-agent-server/AGENTS.md`, including the shared 10-minute local verification budget, JDK 21, matching source-built SDK and isolated Maven repository. An internal Java extraction has no meaningful global-CLI behavioral probe: record the installed `qwen --version` as environment evidence only, and establish the baseline through the existing Java fixtures. No model call or JavaScript build is needed for those selected fixtures.
The focused plan is recorded under `.qwen/e2e-tests/runtime-execution-dispatcher.md`. Pin reservation deduplication, synchronous-failure cleanup, invocation removal before settlement/UNKNOWN and shutdown that preserves the underlying invocation and borrowed resources. Reuse `RuntimeBrokerServiceTest`, `RuntimeSshAcknowledgementTest`, `Issue13183RegressionTest` and `RuntimeBrokerHttpServerTest` for cancellation/fencing, v3/private CSI, receipt and HTTP integration behavior. Run compilation, both Checkstyle goals and the selected tests; check actual report counts and skips.
The baseline passed 280 selected tests. After extraction, the same suites plus five dispatcher cases passed 285 tests with no failures, errors or skips; both Checkstyle goals passed. Verification used JDK 21 on macOS with the source-matching SDK and isolated Maven repository. Setup, the initial unused-import Checkstyle failure and the corrected run used 252.587 seconds of the shared local budget. The cancellation witness proves preservation of the physical invocation and borrowed resources; exact wrapper cancellation is checked by source equivalence rather than a private-state test accessor.
Acceptance requires one dispatch-state owner; no cancellation callback or raw invocation-set injection; unchanged public contracts/schema and dispatch algorithms; preserved close/completion order; focused regressions passing; synchronized bilingual docs; and two clean self-audit passes. Full MySQL 8.4, bundled Hosted/fault, cross-platform and full SpotBugs gates remain CI work. Local fixtures do not establish those results, and an isolated pass does not clear the earlier wider failure.
## Risks and open questions
The material risks are moving the invocation marker relative to settlement, changing reservation cleanup, losing original-generation routing, changing v3 deadline/receipt precedence, or taking over executor/transport cancellation. Preserve method bodies and caller lock envelopes, then exercise the existing witnesses and the new owner-focused tests.
There is no open product decision. Further provisioning, Session lifecycle or persistence extraction is outside this increment. The unresolved baseline cold-recovery failure remains a separate investigation and acceptance gap.
@@ -0,0 +1,76 @@
# Runtime 执行分发器状态归属
[English](2026-11-01-runtime-execution-dispatcher.md) | [简体中文](2026-11-01-runtime-execution-dispatcher.zh-CN.md)
- 状态:本地已实现,定向验证通过;完整 CI 验收待完成。
- 日期:2026-11-01
- 基线:`2aa908dc70785e38299a43964e064dbafaf7b878`,位于 #16029 之后。
## 问题与当前状态
执行流程已有明确持有者,但 `RuntimeBrokerService` 仍持有分发预留项、实际在途调用、分发租约续期及完成处理。取消流程接收在途集合和逐调用传入的 service 回调。如果只搬运 transport 调用,状态和清理决策仍会分散在多个持有者中。
本增量在现有 runtime-broker 模块内抽取一个 package-private 的 `RuntimeExecutionDispatcher`。保留当前算法、准入边界、同步异常、异步完成及关闭顺序,不修改公共 API、HTTP 路由、SQL/schema、配置、依赖或产品行为。
#16029 的早期完整门禁中,`WorkspaceCsiHostedFilesTest` 冷恢复用例在该任务进入 MySQL 集成阶段前失败,尚未完成根因定位。单独保留这份证据。开发本次抽取不代表已修复基线失败,也不代表已合并重构完成了完整集成验收。
## 归属与消费方
| 持有者 | 职责 | 直接消费方 |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| `RuntimeExecutionDispatcher` | 分发预留项、实际 invocation 集合、claim 到 EXECUTING 的转换、分发续租、transport 调用、v3 结果等待和持久 SSH 回执确认 | service 的 execution/start 与 drain 路径;取消流程 |
| `RuntimeExecutionState` | 已有持久执行归属、带 fencing 的结算、UNKNOWN 转换和后台同胞行结算 | 分发器及现有执行流程 |
| `RuntimeExecutionCancellation` | 持久取消意图、物理取消和 prepared provider 确认 | service 取消门面 |
| `RuntimeBindingRegistry` | 已有本地 Session context、路由身份和 Binding 失效 | service 与执行协作者 |
| `RuntimeBrokerSupport` | 已有共享规则,以及不变的 detached/no-start 回执投影和续租间隔计算 | service 与分发器 |
| `RuntimeBrokerService` | 公共准入及 payload/grant 校验、供应、Session/drain/stop 编排、共享关闭屏障、两个 executor 及 provisioner 生命周期 | 现有 adapter 与装配 |
分发器借用现有 repository、Registry、执行状态、provisioner、transport、publication verifier、clock、租约/窗口参数、executor 和关闭屏障。依赖保持为显式构造参数,不增加通用 context 容器、接口或 Spring bean。
完整直接消费方清单为:service 的创建与两个 start 入口族、drain 确认扫描和关闭顺序;取消流程的两次 invocation 检查与未发送分发分支;测试辅助入口 `BrokerServiceTestAccess.pollDelay`。service 的其他回执路径通过 Support 继续使用共享的 `sshNoStartReceipt` 与 `isDetachedCapture` 投影。Repository 与 transport 契约保持不变。
## 分发准入与执行
将三个 `beginDispatch` 重载、`dispatch`、`enterExecuting`、reference/capture 投影及 `DispatchRenewal` 一起迁移。私有 CSI 限制、Harness 准入和 deferred payload 拒绝保留原先顺序,均在取得预留项之前执行。现有 Session context 锁区域继续位于调用方;分发器不为准入或分发新增锁。
预留 map 使用 `putIfAbsent`,同步失败和异步完成时使用 `remove(executionId, exactFuture)` 条件移除。它覆盖 claim/fencing,也覆盖实际调用。独立 invocation 集合只在续租开始后、调用 transport 前登记。取消通过 `isInvoking(executionId)` 观察该集合,不再接收可变集合。
保留 claim、带 fencing 的 EXECUTING 转换、owner/代际校验、可用租约校验、启动续租和 transport 调用的顺序。已取消但尚未发送的执行继续经原分发路径结算,不使用新的取消结算算法,也不重放 UNKNOWN 工作。
完成时先移除实际 invocation 标记,再持久化终态结果或 UNKNOWN。保留后台同胞行结算,以及持久写入之后 best-effort 的 SSH 确认。transport 失败、空结果或普通结算异常继续映射为 UNKNOWN。同步 `RuntimeBrokerException` 原样保留;其他同步运行时/JVM 失败保留 `runtime_execution_dispatch_failed` 包装及预留项清理。
## V3 结果与 SSH 确认
v3 结果等待属于分发调用闭包,原样迁移。保留 publication 证据优先读取、dispatch/Binding 身份校验、租约/截止时间拒绝、显式 `not_started`、detached 结果、不可重试错误处理,以及从 100 ms 翻倍、上限 2 s 的退避。仅供测试的退避访问器改为引用新持有者。
将 `acknowledgePersistedTools` 迁入分发器,因为分发完成和 service 的 drain 扫描都消费它。drain 与物理 Runtime stop 仍由 service 编排。共享且无状态的 `sshNoStartReceipt`、`isDetachedCapture` 和 `renewalDelayMillis` 迁入已有 Support,不复制规则,也不新增协作者。
## 取消与关闭
取消接收具体分发器,将直接读取 invocation 集合改为 `isInvoking`,将 `BiConsumer` 回调改为 `beginDispatch(context, requested)`。归属读取、持久意图、准入拒绝、control guard 和物理取消顺序保持不变。
service 继续是 `closed` 的唯一写入方,并持有两个 executor 和 provisioner。关闭顺序保持为:设置关闭屏障;取消 Registry 操作;取消分发预留 future;取消对账操作;关闭协调与续租 executor;关闭 provisioner。
`cancelPendingOperations` 只取消已有预留包装 future,不取消底层 transport stage,不清空实际 invocation 标记,也不关闭借用资源。不给 `beginDispatch` 增加新的关闭检查;改变已准入操作的处理方式超出本次抽取范围。
## 文件与迁移
新增 `RuntimeExecutionDispatcher`;更新 service 构造与委托、取消流程、共享 Support,以及仅供测试的退避访问器。新增分发器定向测试,保留现有 service、HTTP、SSH 和协议回归。为执行流程文档的两种语言加入本设计链接,同步更新当前状态归属说明。
基线 dry-run 后开始实现:迁移状态与完整分发闭包;在取消流程之前装配分发器;替换全部直接消费方;更新测试及双语设计。保持机械迁移,审阅完整 diff,检查是否意外改变算法或异常边界。
## 验证与验收
遵循 `packages/managed-agent-server/AGENTS.md`,包括共享的 10 分钟本地验证预算、JDK 21、匹配源码构建的 SDK 和隔离 Maven 仓库。内部 Java 抽取没有有效的全局 CLI 行为探针:只记录已安装 `qwen --version` 作为环境证据,通过现有 Java fixture 建立基线。所选 fixture 不需要模型调用或 JavaScript 构建。
定向计划记录在 `.qwen/e2e-tests/runtime-execution-dispatcher.md`。固定预留去重、同步失败清理、在结算/UNKNOWN 之前移除 invocation,以及关闭时保留底层调用与借用资源。复用 `RuntimeBrokerServiceTest`、`RuntimeSshAcknowledgementTest`、`Issue13183RegressionTest` 和 `RuntimeBrokerHttpServerTest` 覆盖取消/fencing、v3/私有 CSI、回执及 HTTP 集成行为。执行编译、两组 Checkstyle 和选定测试,检查实际报告数量及跳过情况。
基线选定的 280 个测试通过。抽取后,同一批测试加上五个分发器用例,共 285 个测试通过,无失败、错误或跳过;两组 Checkstyle 均通过。验证在 macOS 上使用 JDK 21、匹配源码的 SDK 和隔离 Maven 仓库。环境准备、首次无用 import 的 Checkstyle 失败及修正后验证共使用本地共享预算 252.587 秒。取消用例证明实际调用及借用资源得到保留;包装 future 的准确取消动作通过源码等价核对确认,不新增访问私有状态的测试入口。
验收要求:分发状态只有一个持有者;取消没有 service 回调或可变 invocation 集合注入;公共契约/schema 与分发算法不变;关闭/完成顺序不变;定向回归通过;双语设计同步;两轮干净自审。完整 MySQL 8.4、打包 Hosted/fault、跨平台及完整 SpotBugs 门禁继续由 CI 负责。本地 fixture 不构成这些结果,单独一次通过也不能消除此前更广范围的失败。
## 风险与待定问题
主要风险包括 invocation 标记与结算顺序改变、预留清理改变、丢失原代际路由、改变 v3 截止时间/回执优先级,以及接管 executor/transport 的取消。保留方法实现与调用方锁区域,再使用现有行为测试和新增持有者测试验证。
没有待定产品决策。进一步的供应、Session 生命周期或持久化抽取不在本增量内。未解决的基线冷恢复失败继续作为独立排查项和验收缺口保留。
@@ -0,0 +1,80 @@
# Runtime Execution Workflow Decomposition
[English](2026-11-01-runtime-execution-workflows.md) | [简体中文](2026-11-01-runtime-execution-workflows.zh-CN.md)
- Status: Implemented locally with focused validation passing; full CI qualification remains pending.
- Date: 2026-11-01
- Prerequisite: the local [Runtime Binding Registry](2026-10-31-runtime-binding-registry.md) slice on baseline `cf7e77810efa776f29b52a3dc4bf24f17d5e80f8`.
## Problem, goals and scope
The Registry gives Binding and Session state an owner, but execution reconciliation, takeover scans and cancellation still live in `RuntimeBrokerService`. Their shared execution reads, fenced settlement and background-hold settlement make a three-way copy of private helpers unsafe. Extract the three workflows with direct module-internal collaborators and one shared execution-state boundary. Preserve current observable behavior, including failure timing, async continuations, writer fencing and cleanup order.
No public interface, HTTP route, schema, protocol, dependency or Runtime launch policy changes. Provisioning, Binding recovery/drain, Binding renewal scheduling and physical Session release stay in the service. This is structural work; full integration acceptance remains separate from implementation completion.
This document records the workflow extraction merged in #16029. The [Dispatcher follow-up](2026-11-01-runtime-execution-dispatcher.md) moves dispatch reservations, invocations and completion together and replaces the original cancellation callback. The ownership table and cancellation wiring below reflect that follow-up.
## Ownership and dependencies
| Component | Owns | Consumers |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `RuntimeBindingRegistry` | Existing local Binding/Session maps and contexts; READY/historical Session validation | Service and all three execution workflows |
| `RuntimeExecutionState` | Existing execution ownership reads, fenced dispatch settlement/UNKNOWN transitions, lapsed-claim fencing and background-row settlement | Dispatch, reconciliation, recovery, cancellation and existing acknowledgement/control paths |
| `RuntimeExecutionReconciler` | Execution-ID lookup reservations, `ExecutionObservationCooldown`, evidence validation and reconciliation CAS | Service observation endpoints and takeover scanner |
| `RuntimeExecutionRecovery` | Bounded takeover scan and background-hold observation/sweep workflows | Acquire/adoption, routine/drain release, background observation and task stop |
| `RuntimeExecutionCancellation` | Durable cancellation intent and physical cancellation/confirmation workflow | Service cancellation endpoint |
| `RuntimeExecutionDispatcher` | Dispatch reservations/invocations, claim transition, dispatch renewal, v3 result waiting, completion and persisted SSH acknowledgement | Service execution/start and drain paths; cancellation |
| `RuntimeBrokerSupport` | Unchanged stateless future/error mapping, payload/reference validation, status/receipt projection and renewal-delay calculation | The extracted collaborators and service |
| Service | Public facade, `closed`, schedulers, provisioner lifetime, Binding/Session lifecycle orchestration | Existing adapters and assembly |
All classes remain package-private in the existing runtime-broker service package; no new interface or Spring wiring is needed. Repositories remain the sole durable authority. Context locks still belong to the Registry, and every compound read/admit/release decision retains its current lock region. The support class shares existing rules across several real callers; it is not a configurable workflow framework.
## Reconciliation design
Move `reconcileExecution`, automatic `observeExecution`, the context-based lookup, its execution-ID future map, the cooldown and result absorption into `RuntimeExecutionReconciler`. The service keeps the public signatures and delegates. The reconciler receives the Registry, execution-state collaborator, execution repository, provisioner, transport, publication verifier, clock, lookup duration, shared close fence and existing cooldown instance. It owns no executor and never calls the service.
Keep this sequence: validate caller IDs; read owned execution; return non-UNKNOWN/terminal answers before requiring a live Session; consult a finished v3 publication if available; otherwise require the original persisted generation and locally attested route; perform a bounded status lookup. Only validated settled evidence resolves UNKNOWN, and a racing cancellation is reread before CAS. UNKNOWN or unreachable is never proof of `not_started`, never dispatches work and never provisions a Runtime.
Explicit reconciliation remains fresh. Automatic observation consults cooldown first. Concurrent requests join the existing execution-ID reservation. On completion, stamp cooldown before removing that exact reservation, then complete its future; JVM Errors do not become cached UNRESOLVED answers. This order prevents a sequential caller between removal and stamping from issuing another worker request. Service close cancels the reconciler's pending reservations at the same point in the original shutdown order.
Takeover lookup retains its extra close checks, original-generation routing, unresolved handling of ordinary failures/invalid status and refusal to swallow an Error. Background `not_started` settlement goes through the shared execution-state collaborator, avoiding a dependency cycle with the scanner.
## Recovery scan and hold design
Move `scanExecutions` into `RuntimeExecutionRecovery`, together with durable background-row pagination, fire-and-forget scan observations, routine/drain sweeps, physical hold control and the existing background observation/task-stop entry points. The dependency set is Registry, execution state, execution/session repositories, transport, reconciler and the shared close fence. It has no service callback or scheduler.
The scan still reads 100-row pages scoped to the saved Session and generation, rereads each candidate before observation and yields through `thenComposeAsync` between candidates/pages. It does not claim dispatch, resend tools or isolate a record merely because it is old. Acquire waits for the execution scan but never for background maintenance observations. Listing/control failures in the background scan remain isolated from acquire.
Both scan and release use the same durable background-row reader. A scan observes without `activeControls`; ordinary controls/sweeps keep their original guard accounting. The routine release sweep only observes, whereas drain alone follows status → stop → status. Only proven exit or no-start evidence settles the hold. Unknown evidence and per-row errors preserve the hold. Backfill/snapshot/removal of the context's background index retain the same lock regions. Physical Session release and its durable transitions remain in the service and call the recovery collaborator before their existing busy check.
Task stop keeps tenant/Harness ownership validation and the original Session route. It may stop only the invocation's own `:process` row. No recovery path creates a replacement Binding or Session.
## Cancellation design
Move cancellation admission, prepared-provider confirmation, physical cancel and cancellation-result validation into `RuntimeExecutionCancellation`. It uses Registry, execution state, execution repository, transport, the concrete dispatcher, coordination scheduler, cancellation duration and close fence. Cancellation queries `isInvoking` and calls `beginDispatch` for the unsent DISPATCHING branch. It receives neither a mutable invocation set nor a per-call service callback. Do not store a service reference or pass maps/private lifecycle helpers as callbacks.
Preserve terminal-receipt ownership reads before requiring a live Session. A prepared provider's cancelled receipt still requires original-worker confirmation when that worker may retain the preparation. Nonterminal cancellation persists intent before sending a worker request. Hold rows and authorized private CSI calls retain their existing refusals. An invocation still running here may receive cancellation after lease lapse; an invocation absent here first follows the repository-clock dispatch fence. UNKNOWN remains UNKNOWN until reconciliation evidence resolves it.
An unsent DISPATCHING record still goes through the existing dispatcher, which reads its durable cancellation intent before deciding whether to execute; replacing that branch with a new direct settlement algorithm would change behavior. Physical cancellation status is validated and settled with the original dispatch owner/generation. A late result after claim lapse is fenced, and live-claim conflicts remain errors. Prepared-provider cancellation retains its bounded result future, 25 ms observation schedule, control accounting and refusal to treat forgotten state as confirmation.
## Shared state, files and migration order
`RuntimeExecutionState` moves the existing owned-execution checks, `settleExecution`, `markUnknown`, `fenceLapsedClaim`, UNKNOWN-on-closed-admission mapping and background settlement CAS loops without changing their algorithms. It receives the three repositories, Broker owner, dispatch lease and clock. `RuntimeBindingRegistry` additionally receives the existing Session repository and resolver for the READY/historical checks used across workflows; non-managed historical reads still revalidate the resolved scope, while managed history uses its saved parent identity. No resolver/admission check is dropped or moved to a cache-only path.
Implement in order: shared unchanged helpers/state and Registry queries; reconciler; recovery and hold maintenance; cancellation; facade wiring and shutdown. The five new implementation files above, the existing service/Registry/cooldown documentation, test constructors and focused reconciliation regression are the affected code surfaces. Update both Registry and structure-review design pairs to link this design and distinguish the earlier slice from the new implementation. The existing contract/schema/adapters remain unchanged.
## Validation and acceptance
Add a deterministic reconciler-level regression for stamp-before-reservation-removal: block cooldown stamping, submit another observation during that interval and assert that it joins without a second worker lookup. Cover both an ordinary status and an ordinary lookup failure, then release the stamp and verify completion/cooldown. The witness must fail if reservation removal is moved before stamping.
Reuse the existing service regressions for explicit versus automatic lookup, timeouts, malformed status, JVM Errors, publication results, concurrent cancellation and shutdown. Recovery regressions must cover multi-page takeover, generation/Session isolation, asynchronous background observations, durable-only holds and routine versus drain release. Cancellation regressions cover terminal ownership, prepared confirmation, unsent dispatch, lapsed claims, sticky intent, cross-Harness refusal and hold/CSI refusals. Keep their assertions and original ordering expectations when moving code.
Within the module's shared 10-minute local verification budget, reuse the source-matching SDK and JDK 21/Maven environment, run affected Java suites with Checkstyle and the minimum subprocess path, and inspect the actual selected reports. Full MySQL 8.4, bundled Hosted/fault recovery and SpotBugs remain required CI qualification; fixture/H2 or test-worker subprocess passes do not establish those results. No JavaScript build is required because this change has no TypeScript/shared-contract change.
Acceptance: all three facade entry families delegate to their named collaborators; lookup reservations and cooldown share one owner; no collaborator reaches back into service internals; cancellation uses the dispatcher without a service callback or raw invocation set; no SQL/schema/public-contract diff; moved algorithms retain their semantics; the ordering witness and focused regressions pass; bilingual documents and two clean self-audit passes are complete. CI acceptance is pending until its required gates run on the accepted revision.
## Risks and open questions
The main risks are altered synchronous exception mapping, completion ordering, guard accounting and cross-generation routing. Mechanical moves and unchanged call-site envelopes reduce these risks, while the existing integration tests exercise them through the public service facade. Shared immutable dependencies are passed directly rather than hidden in a generic context object.
The original #16029 slice deliberately limited its callback exception to cancellation's unsent-dispatch branch. The Dispatcher follow-up replaces it with a concrete collaborator while preserving cancellation semantics and moving dispatch state and completion together. No product decisions remain open. Further provisioning or Session lifecycle extraction is outside this increment.
@@ -0,0 +1,80 @@
# Runtime 执行流程拆分
[English](2026-11-01-runtime-execution-workflows.md) | [简体中文](2026-11-01-runtime-execution-workflows.zh-CN.md)
- 状态:本地已实现,定向验证通过;完整 CI 验收仍待完成。
- 日期:2026-11-01
- 前置条件:基于 `cf7e77810efa776f29b52a3dc4bf24f17d5e80f8` 的本地 [Runtime Binding Registry](2026-10-31-runtime-binding-registry.zh-CN.md) 切片。
## 问题、目标与范围
Registry 明确了 Binding 与 Session 状态持有者,但执行对账、接管扫描与取消仍在 `RuntimeBrokerService` 中。它们共享执行读取、带 fencing 的结算和后台 hold 结算,不能安全地把私有 helper 复制三遍。用直接依赖的模块内部协作者拆出三个流程,并建立一个共享执行状态边界。保留当前可观察行为,包括失败时机、异步 continuation、writer fencing 和清理顺序。
不修改公共接口、HTTP 路由、schema、协议、依赖或 Runtime 启动策略。供应、Binding 恢复/排空、Binding 续租调度和物理 Session 释放仍在服务中。这是结构调整;完整集成验收与实现完成分别记录。
本文记录 #16029 已合并的执行流程拆分。[Dispatcher 后续设计](2026-11-01-runtime-execution-dispatcher.zh-CN.md) 将分发预留项、在途调用和完成处理一起迁移,并替换原有取消回调。下方归属表和取消装配说明反映该后续实现。
## 归属与依赖
| 组件 | 持有职责 | 调用方 |
| ------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `RuntimeBindingRegistry` | 已有本地 Binding/Session map 与 context;READY/历史 Session 校验 | 服务与全部三个执行流程 |
| `RuntimeExecutionState` | 已有 execution 归属读取、带 fencing 的分发结算/UNKNOWN 转换、过期 claim 隔离和后台行结算 | 分发、对账、恢复、取消及现有确认/control 路径 |
| `RuntimeExecutionReconciler` | 按 execution ID 预留的查询槽位、`ExecutionObservationCooldown`、证据校验与对账 CAS | 服务观察入口与接管扫描 |
| `RuntimeExecutionRecovery` | 有界接管扫描与后台 hold 观察/清理流程 | acquire/接管、普通/排空 release、后台观察与任务停止 |
| `RuntimeExecutionCancellation` | 持久取消意图与物理取消/确认流程 | 服务取消入口 |
| `RuntimeExecutionDispatcher` | 分发预留项/在途调用、claim 转换、分发续租、v3 结果等待、完成处理和持久 SSH 确认 | 服务的 execution/start 与 drain 路径;取消流程 |
| `RuntimeBrokerSupport` | 不变的无状态 future/错误映射、payload/reference 校验、status/回执投影和续租间隔计算 | 抽出的协作者与服务 |
| 服务 | 公共门面、`closed`、scheduler、provisioner 生命周期、Binding/Session 生命周期编排 | 现有 adapter 与装配 |
全部类保留在现有 runtime-broker service package 内,采用 package-private,不新增接口或 Spring 装配。Repository 仍是唯一持久权威。context 的锁继续归 Registry,每个复合读取/准入/释放决策保留当前锁区域。support 类只共享多个真实调用方已有的规则,不引入可配置流程框架。
## 对账设计
将 `reconcileExecution`、自动 `observeExecution`、基于 context 的查询、按 execution ID 索引的 future map、cooldown 和结果吸收迁入 `RuntimeExecutionReconciler`。服务保留公共签名并委托。对账器接收 Registry、执行状态协作者、execution repository、provisioner、transport、publication verifier、clock、查询时限、共享关闭屏障和现有 cooldown 实例。不持有 executor,不调用服务。
保留以下顺序:校验调用方 ID;读取属于调用方的 execution;在要求活跃 Session 之前返回非 UNKNOWN/终态结果;若存在已完成的 v3 publication,则读取它;否则要求原持久代际与本地已核验路由,再执行有时限的 status 查询。只有合法的 settled 证据才能结算 UNKNOWN,并在 CAS 前重新读取并发取消。UNKNOWN 或不可达绝不构成 `not_started` 证明,不触发分发或 Runtime 供应。
显式对账继续读取新状态,自动观察先读 cooldown。并发请求加入已有 execution ID 预留项。完成时先 stamp cooldown,再移除准确的预留项,最后完成 future;JVM Error 不得缓存为 UNRESOLVED。这样可防止顺序调用方在移除与 stamp 之间发起第二次 worker 查询。服务关闭在原有关闭顺序的同一位置取消对账器待完成的预留项。
接管查询保留额外的关闭检查、原代际路由、普通失败/非法 status 的 unresolved 处理,以及不吞掉 Error 的规则。后台 `not_started` 结算经共享执行状态协作者执行,避免与扫描器形成依赖环。
## 恢复扫描与 hold 设计
把 `scanExecutions` 连同持久后台行分页、后台扫描观察、普通/排空清理、物理 hold control 和现有后台观察/任务停止入口迁入 `RuntimeExecutionRecovery`。依赖为 Registry、执行状态、execution/session repository、transport、对账器和共享关闭屏障。不回调服务,也不持有 scheduler。
扫描继续按已存 Session 与代际读取每页 100 行,观察前重读候选行,并通过 `thenComposeAsync` 在候选项/页面之间让出执行。它不 claim dispatch、不重发工具,也不因记录久远就隔离它。acquire 等待 execution 扫描,但不等待后台维护观察。后台扫描的列举/control 失败仍与 acquire 隔离。
扫描与 release 共用持久后台行读取。扫描观察不持有 `activeControls`,普通 control/清理保持原 guard 计数。普通 release 清理仅观察,只有 drain 执行 status → stop → status。仅已证明退出或未启动的证据能结算 hold;未知证据与逐行错误保留 hold。context 后台索引的回填、快照和移除保留原锁区域。物理 Session 释放及其持久转换仍归服务,在已有 busy 检查前调用恢复协作者。
任务停止保留 tenant/Harness 归属校验和原 Session 路由,只能停止该 invocation 自己的 `:process` 行。任何恢复路径都不创建替代 Binding 或 Session。
## 取消设计
把取消准入、prepared provider 确认、物理取消与结果校验迁入 `RuntimeExecutionCancellation`。它使用 Registry、执行状态、execution repository、transport、具体分发器、协调 scheduler、取消时限和关闭屏障。取消流程通过 `isInvoking` 查询在途调用,并在未发送的 DISPATCHING 分支调用 `beginDispatch`,不再接收可变 invocation 集合或逐调用传入的 service 回调。不保存 service 引用,不把 map/私有生命周期 helper 作为回调传递。
保留在要求活跃 Session 前读取终态回执归属的顺序。若原 worker 可能仍保留 preparation,prepared provider 的已取消回执仍需原 worker 确认。非终态取消先持久化意图,再发送 worker 请求。hold 行和已授权的私有 CSI 调用保留原拒绝规则。租约过期后,本进程仍运行的 invocation 可继续接收取消;本进程未运行的 invocation 先按 repository 时钟执行 dispatch fence。UNKNOWN 只在对账证据足够时才能结算。
未发送的 DISPATCHING 记录继续走已有 dispatcher,由它读取持久取消意图后决定是否执行;用新的直接结算算法替代该分支会改变行为。物理取消 status 按原 dispatch owner/generation 校验并结算。claim 过期后的迟到结果被隔离,活跃 claim 冲突继续报错。prepared provider 取消保留有时限的结果 future、25 ms 观察调度、control 计数,以及不把已遗忘状态当作确认的规则。
## 共享状态、文件与迁移顺序
`RuntimeExecutionState` 原样迁移 execution 归属检查、`settleExecution`、`markUnknown`、`fenceLapsedClaim`、准入关闭时保留 UNKNOWN 的映射和后台结算 CAS 循环,不改变算法。接收三个 repository、Broker owner、dispatch lease 和 clock。`RuntimeBindingRegistry` 额外接收现有 Session repository 与 resolver,以提供跨流程使用的 READY/历史检查;非 managed 历史读取继续重验解析的 scope,managed 历史使用已存父身份。不删除 resolver/准入检查,也不把它们改成仅依赖缓存的路径。
实现顺序:共享的不变 helper/状态与 Registry 查询;对账器;恢复与 hold 维护;取消;门面装配和关闭。受影响代码为上面的五个新实现文件、已有 service/Registry/cooldown 说明、测试构造器和定向对账回归。同时更新 Registry 与结构评审的两组双语设计,链接本设计并区分早期切片与本次实现。现有 contract/schema/adapter 不变。
## 验证与验收
增加确定性的对账器级先 stamp 后移除预留项回归:阻塞 cooldown stamp,在该窗口内再次观察,断言加入已有操作且不产生第二次 worker 查询。覆盖普通 status 与普通查询失败,再放开 stamp 并验证完成/cooldown。把预留项移除移到 stamp 前时,该测试必须失败。
复用 service 回归覆盖显式/自动查询、超时、非法 status、JVM Error、publication 结果、并发取消与关闭。恢复回归须覆盖多页接管、代际/Session 隔离、异步后台观察、仅持久存在的 hold 和普通/排空 release 差异。取消回归覆盖终态归属、prepared 确认、未发送分发、过期 claim、粘性取消意图、跨 Harness 拒绝和 hold/CSI 拒绝。迁移代码时保留这些断言及原顺序预期。
在模块共享的 10 分钟本地验证预算内,复用匹配源码的 SDK 和 JDK 21/Maven 环境,运行受影响 Java 测试、Checkstyle 和最小子进程路径,并检查真实选中的报告。完整 MySQL 8.4、打包 Hosted/fault 恢复和 SpotBugs 仍需 CI 验收;fixture/H2 或测试 worker 子进程通过不构成这些结果。本次无 TypeScript/共享契约变化,不需要 JavaScript 构建。
验收要求:三个公共入口族委托给对应协作者;查询预留项与 cooldown 归同一持有者;协作者不访问 service 内部;取消流程直接使用分发器,不接收 service 回调或可变 invocation 集合;SQL/schema/公共契约无 diff;迁移算法语义不变;顺序回归与定向测试通过;双语文档及两轮干净自审完成。在被验收版本运行所需门禁前,CI 验收仍待完成。
## 风险与待定问题
主要风险是同步异常映射、完成顺序、guard 计数和跨代际路由改变。通过机械迁移和保留调用点包络降低风险,现有集成测试继续经公共服务门面验证它们。共享的不可变依赖直接传递,不隐藏在通用 context 对象中。
原 #16029 切片刻意将回调例外限制在取消的未发送分发分支。Dispatcher 后续实现用具体协作者替换它,保持取消语义,并将分发状态与完成处理一起迁移。没有待定产品决策。进一步拆分供应或 Session 生命周期不在本增量范围内。
@@ -0,0 +1,554 @@
# ManagedAgentStore and ManagedSessionStore decomposition
[English](2026-11-02-managed-store-decomposition.md) | [简体中文](2026-11-02-managed-store-decomposition.zh-CN.md)
## Status and scope
This is the implementation contract for the accepted Store decomposition. The baseline is main `e3c7a6d7efc6df6c58ffa8ddd9c42dbb0e1784c4`. R0 ([#16061](https://github.com/QwenLM/qwen-code/pull/16061)) merged as `1bdad7d4b7949a6f8370c95965ec55901e75f67a`; its [full gate](https://github.com/QwenLM/qwen-code/actions/runs/41322812090) was still running when R1 preparation began. Merge and qualification are separate evidence. [#16029](https://github.com/QwenLM/qwen-code/pull/16029) is merged and adds no prerequisite work.
R1 delivers this complete bilingual contract, the entry/transaction/caller inventory below and a lock-order audit that follows collaborators across source files. A1–A9 and B0–B6 merged together in [#16090](https://github.com/QwenLM/qwen-code/pull/16090) as `d9d6fc4a4b8e654d405dbb1d01bf3404e6435a1c`, retaining the stage ownership and acceptance checklist below. Both facades remain stable; implementation stays inside persistence. The configured full Managed Agent gate passed for its head `66d23ceb1a5bee7e9cb6362cfc238eb64297f3e6`; Store-specific macOS/Windows functional qualification remains uncovered. The cleanup follow-up below has separate acceptance evidence.
Related designs: [structure review](2026-10-31-managed-agent-structure-review.md), [Agent constructor wiring](2026-11-01-managed-agent-store-wiring.md), [Maven boundaries](2026-10-29-managed-agent-maven-modules.md).
## Problem and decisions
The two Stores mix transaction entry points with SQL, mutable business state and several unrelated operation state machines. Moving methods by name would conceal side effects and can disable the existing single-file lock-order audit. Stable facades and explicit component ownership keep transaction behavior and callers reviewable throughout the extraction.
Keep public business interfaces, DTOs, exception mapping and package-visible compatibility bridges. New collaborators are package-private `final` classes in the same package, constructed once by the facade. Add no Spring beans, Maven modules, public business interfaces, mutable dependency setup or generic Context/Support container. Existing worktree, definition and skills components retain their responsibilities.
Dependencies use concrete types in one direction. Do not introduce facade callbacks or bundles of constructor functions to escape a cycle. Existing lazy Spring cycles, per-call fencing callbacks and compatibility value types keep their current meaning. If an extraction needs a shared rule, move its actual dependency closure with the first consumer; do not build unused helper frameworks.
## Agent responsibility ownership
| Slice | Component | Responsibility |
| ----- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| A1 | `AgentSessionQueries` | Ordinary Session/Turn reads, paging, batch summaries, worktree descriptions and lifecycle presentation views |
| A2 | `AgentEventJournal` | Public event writes, sequence and identity, deduplication, event reads and commit notifications |
| A3 | `AgentMessageProjection` | Materialization, message pages, coverage, replay floor and repair after retraction |
| A4 | `AgentSessionPolicies` | Model, approval, MCP and extension admission/configuration constraints |
| A5 | `AgentSessionCommands` | Turn input/cancel commands, rename, general Session mutation, command receipts and settlement |
| A6 | `AgentSessionAdmissions` | Ordinary/workspace/child/internal/CSI Session admission and replay; definition, skills and worktree preparation |
| A7 | `AgentLifecycleOperations` | Close/archive/delete/unarchive, delivery scans and shared operation claim/retry/completion |
| A8 | `AgentExecutionOperations` | Cwd, context compression, task cancellation and unknown-acceptance settlement, including compression projection backfill |
| A9 | `AgentTurnExecution` | Dispatch, leases, Harness admission/events, recovery wait, retries and Turn terminal state |
`AgentStoreRows` owns shared Session/Turn row mapping and locked row reads. Commands owns shared receipts/settlement, EventJournal owns event identity and notification, and MessageProjection owns projection repair. Higher-level admission, lifecycle and execution workflows depend on these concrete lower-level collaborators. Move operation scans with their state machines.
Queries must not become a facade-shaped dependency of every writer. Writers use Rows for shared row access. `contextCompressionState(s)` performs projection backfill and belongs to ExecutionOperations. `deliverableRenames` changes delivery state and belongs to Commands. Preserve unannotated writes such as `markRenameSubmitted` as unannotated entries.
The worktree presentation entries `findSessionWorktree(s)` and `listSessions` can persist BLOCKED through the existing `SessionWorktreeStore` verification and its `REQUIRES_NEW` quarantine transaction. Exclude these three names from A1's pure-query slice; keep their complete implementation until A6 moves the worktree collaboration into the intended Queries presentation path. Keep that path explicitly side-effecting, reuse the existing worktree component and add no duplicate wrapper or new transaction. A1's boundary is smaller than the final Queries responsibility.
## Journal responsibility ownership
| Slice | Component | Responsibility |
| ----- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| B0 | Existing facade construction | Replace eight injection setters/nine references with construction-time assembly |
| B1 | `JournalWriterLifecycle` | Acquire, renew, seal, quiesce, release, block recovery and live-writer checks |
| B2 | `JournalLifecycleAuthorization` | Ordinary/lifecycle authorization, approval selection and lifecycle dispatch authorization |
| B3 | `JournalHistoryQueries` | Restore, paged/recent transactions, command transaction lookup and initial-journal integrity |
| B4 | `JournalResourceWrites` | Instructions, skills, inherited resources, tool-result publication and resource writes inside commit |
| B5 | `JournalResourceReads` | Ordinary/snapshot/owner reads, verification updates, quarantine, read leases and receipts |
| B6 | `JournalCommit` | Ordinary/lifecycle commit, CSI terminal preflight/commit, associated projections and notifications |
`JournalStoreRows` owns shared row access/mapping and the integrity checks required to interpret those rows. B1 already needs activation/head checks and resource-receipt validation: migrate `activationLeaseRow`, `restoreHead`, `verifyStoredResource` and its `toolResultLimit` dependency with their first consumer. The validation can issue read-only capture/quarantine lookups; it does not stream objects, acquire read leases, quarantine objects or update verification timestamps. Those side effects remain in ResourceReads. Move other helpers only when actually used.
`JournalFencing` owns credentials, writer and CSI fencing, plus bounded lock-wait error mapping. Keep its checks at their existing small-grained call sites. WriterLifecycle, LifecycleAuthorization and resource operations use Rows/Fencing; Commit depends on the concrete resource/authorization collaborators it actually calls. Do not invert the graph by making WriterLifecycle call back through the facade to HistoryQueries or ResourceReads.
The existing `ensureSessionRoot` is also shared by writer acquisition and lifecycle authorization. Put this explicit, narrow root initialization in Rows when B1 first uses it: preserve its root lock and event-stream INSERT. Rows is therefore a row-access boundary, not a read-only service. Do not move writer or operation state machines into it.
## Transactions, locks and atomicity
Every facade entry retains its original transaction manager, propagation, isolation, read-only flag and absence of an annotation. This includes `MANDATORY`, `NOT_SUPPORTED` and `READ_COMMITTED`. Ordinary collaborator objects create no transaction proxy and introduce no `REQUIRES_NEW`. Existing programmatic transactions keep their original placement and timeout.
For paths locking both Session and Turn, Session remains first. Placement, retention, root and head locks retain each path's existing order. CSI continuation exceptions and bounded lock-wait/deadlock mapping remain explicit; do not impose a new universal order.
Commit resource changes, extension/action/message projections, journal transaction and head update atomically. Preserve duplicate-return positions, writer/CSI rechecks, configuration read timing and notification timing. Rollback and replay must not add notifications; standalone behavior without synchronization remains unchanged.
Resource reads are not plain queries. Ordinary and snapshot reads retain their distinct verification/quarantine behavior and retention lease handling. Owner reads continue to reject an ambient transaction and use short admission transaction, object read, short recheck/receipt/cleanup transaction. Compression projection backfill keeps its programmatic transaction.
Existing external `REQUIRES_NEW` boundaries remain: worktree quarantine and the snapshot caller's isolated `READ_COMMITTED` template. The ban is on introducing new transaction boundaries, not deleting those already required by callers.
Do not change SQL, digests, JSON or protocol bytes, schema, HTTP/OpenAPI contracts or algorithms while extracting.
## Construction and compatibility
B0 resolves required dependencies at construction while retaining standalone defaults. The optional lifecycle pair remains disabled as a pair when either member is absent. CSI bindings still accept only a JDBC repository using the same data source. Update both maintenance entry points, manually assembled fixtures and factory-created Spring beans together. Verify missing required/optional dependencies, the real lazy cycle, proxy identity, actual `@Bean` factories and standalone construction.
Preserve publication/snapshot methods, `PublicationWriter`, `TerminalCandidate`, same-data-source checks and existing static lock tools on the original facade. CSI terminal credential checks, preflight and commit must use the same physical connection bound to the caller's authorization. Agent CSI admission/credential bridges and static binding mapping also remain compatible. Exact bridge ownership is listed in the entry inventory.
## Lock-audit implementation
Keep the JDK parse-tree audit and its bounded resolution model. Load the facade with the same-package `Agent*.java` or `Journal*.java` collaborator family, rather than depending on a manually updated per-class allowlist. Other persistence files retain their existing boundary scan. New SQL-bearing files must still change the exactly pinned sibling populations visibly.
Harvest members across the selected files using owner, name and parameter types. Keep source-specific imports and constant environments separate; qualify shared constants so one file cannot overwrite another file's SQL spelling. Cross-check parsed members against compiled classes for every selected source, including collaborators and nested types.
Follow a field receiver only when the reference is final and its declared class is a parsed concrete final type. Resolve each field type in its declaring class. Support bare fields and explicit `this.field`; parameters/locals shadow bare fields without shadowing explicit field access. An external imported type must not borrow members or SQL constants from a same-named parsed collaborator. Keep unresolved, polymorphic, overload-ambiguous, generic/local-class and deferred-body cases fail-closed when a candidate reaches a Session lock or Turn row. Do not broaden ambiguity exemptions or delete population assertions to make moved code pass.
Retain current facade entry sets, transaction checks, sibling boundary sets and ambiguity/deferred-body assertions. Collect entry populations under their facade owner so a same-named collaborator cannot satisfy them; order, ambiguity and compiled-member checks still cover the whole source family. Regression examples must cover a multi-file transitive delegate, moving the Session lock after Turn access, removing the lock entirely (caught by the pinned locker population), hiding it behind an unresolved receiver, shadowing, ambiguous overloads and file-specific constants/imports.
The audit establishes source-level ordering, not transaction runtime semantics or database deadlock freedom. Each implemented stage still needs its relevant runtime and MySQL evidence.
## Implementation checklist
This table is the sole stage checklist, mirrored completely in the Chinese version. The entry inventory is its method-level detail, not another independent status list. Update both languages with each stage's implementation, exact tested revision, relevant evidence and remaining gaps. The stages are delivered together; none is accepted merely because its code has moved.
| Order | Deliverable | Entry inventory | Status / evidence |
| --------- | ---------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
| R0 | Complete Agent constructor wiring and actual Spring factory regression | Existing wiring design | #16061 merged; current wiring covered by #16090 full gate; historical R0 failures retained |
| R1 | Bilingual contract/inventory and cross-file lock audit | Both inventories below | #16090 merged; 59 lock-audit cases and reactor Checkstyle passed in the full gate |
| A1 | Ordinary Agent queries and first required row helpers | Agent A1 | #16090 merged; baseline full gate passed |
| A2 | Event journal | Agent A2 | #16090 merged; baseline full gate passed |
| A3 | Message projection | Agent A3 | #16090 merged; baseline full gate passed |
| A4 | Session policies | Agent A4 | #16090 merged; baseline full gate passed |
| A5 | Commands, receipts and settlement | Agent A5 | #16090 merged; baseline full gate passed |
| A6 | Session admission | Agent A6 | #16090 merged; baseline full gate passed |
| A7 | Lifecycle operations | Agent A7 | #16090 merged; baseline full gate passed |
| A8 | Execution operations | Agent A8 | #16090 merged; baseline full gate passed |
| A9 | Turn execution and Agent facade cleanup | Agent A9 | #16090 merged; baseline full gate passed |
| B0 | Session constructor wiring | Journal B0 | #16090 merged; baseline full gate passed |
| B1 | Rows, fencing and writer lifecycle | Journal B1 | #16090 merged; baseline full gate passed |
| B2 | Lifecycle authorization | Journal B2 | #16090 merged; baseline full gate passed |
| B3 | History queries | Journal B3 | #16090 merged; baseline full gate passed |
| B4 | Resource writes | Journal B4 | #16090 merged; baseline full gate passed |
| B5 | Resource reads | Journal B5 | #16090 merged; baseline full gate passed |
| B6 | Ordinary/CSI terminal commit and Journal facade cleanup | Journal B6 | #16090 merged; baseline full gate passed |
| Follow-up | Reviewed cleanup and architecture guards | Both facades / A3 | Implemented; 232 focused cases and Checkstyle pass; full CI pending |
## Validation and release criteria
R1 has no CLI/HTTP behavior change: a global CLI baseline and model calls do not exercise this source audit. Use synthetic multi-file fixtures and the real persistence source tree, compile the verification module, run `SessionRowLockOrderTest` and reactor Checkstyle. Check bilingual structure, links, formatting and the complete diff. Follow the module's shared local Java budget, preserve failures and reuse only unchanged inputs; a new SHA or continuation does not reset it. Prior 272-case wiring evidence does not qualify this changed checker.
Historical local evidence is retained under `.qwen/investigations/managed-store-split-implementation/`: R1 passed 56 focused lock-audit cases and eight reactor Checkstyle checks; A1–A9/B0–B6 passed 260 distinct cases in 17 completed suites after two wiring-fixture repairs. Both later bounded Maven commands timed out, and the original shared ledger remains 598.655/600 seconds. Those local commands are not relabeled as successful; their then-pending checks are addressed by the subsequent baseline CI evidence below. Historical R0 failures remain archived without independent reclassification.
The [full Managed Agent gate](https://github.com/QwenLM/qwen-code/actions/runs/41426848917) passed for #16090 head `66d23ceb1a5bee7e9cb6362cfc238eb64297f3e6`. Its actual test merge was `61979c75f943e7383748c75beed02cf1dde977e5`; all 86 changed blobs matched the head. Unit/H2: 6,098 cases, 17 skipped; MySQL 8.4.6: 1,242 cases, no skips; Hosted: 167 cases, four skipped; fault: 45 cases, no skips; Hosted process gates: two cases, no skips; O4: 59 cases, no skips. All completed with zero failures/errors. The 59 lock-audit cases, six production SpotBugs checks, eight reactor Checkstyle checks, Hosted failover and both EventTransport profiles passed. Skipped cases are not passes. The macOS/Windows lanes compiled the reactor and ran SDK/Runtime Broker tests, not Store functional suites; that platform gap remains explicit. This evidence qualifies the merged baseline's configured gates and does not qualify later cleanup changes, real clusters or deployment.
The cleanup follow-up starts from main `73474ef094ea1c6bb2a95a9a2a33a28b0ec58e9e`. It removes 18 redundant collaborator default overloads and three unused internal methods. Facade entries supply exactly the original defaults directly to the full-parameter collaborator methods, preserving signatures, transaction attributes and dispatch through the collaborator rather than another facade overload. Message rows map directly to `ItemRecord`; `List.copyOf` preserves the previous immutable page list. The optional Rows forwarding cleanup remains deferred. Architecture guards reject Spring meta-annotations including `@Service`/`@Repository` and SQL keywords regardless of case, with negative fixtures; a paging assertion protects list immutability. Only the deleted `beginLifecycle/9` member and `ItemRow.toRecord/0` exemption leave the lock audit; facade populations, compiled-member coverage and fail-closed negative cases remain intact.
Cleanup local verification passed 232 cases across 14 selected suites with zero failures, errors or skips, including all 59 lock-audit cases and five decomposition cases. All eight reactor Checkstyle checks passed. Static substitution checks confirm the same 18 canonical default paths and 633 other parsed member bodies unchanged; these checks are not runtime evidence. The initial trailing-blank-line Checkstyle failure was repaired and retained in the cleanup investigation. Later source edits only adjust comments and call wrapping; final Checkstyle and default-path checks cover those edits. Full verify, SpotBugs, real MySQL, Hosted/fault and the new revision's full gate remain pending CI; Store-specific macOS/Windows functional qualification remains uncovered. The cleanup is a separately requested change after #16090 merged, with its own shared 600-second local budget; the original PR's ledger and evidence remain unchanged and do not qualify the new inputs.
Qualification of each implemented stage must cover tenant/actor isolation, cursor semantics and batch query counts; admission/replay including definition/skills/worktree; sequence/dedup/projection gaps/retraction/replay floor; after-commit notification and rollback/replay silence; lease generation/token/expiry and unknown-acceptance non-redispatch; atomic journal rollback/replay; resource corruption/read-lease invalidation and owner-read cleanup; publication/snapshot lock order and CSI same-connection atomicity.
Use real MySQL evidence for SQL/concurrency paths. CI owns full verify, SpotBugs, complete MySQL, Hosted/fault and cross-platform checks. The current push-time fast tier omits required qualification; the full Managed Agent gate on the accepted revision is the release criterion. Keep every unrun gate explicit.
Each slice needs two consecutive clean complete-diff self-audits and independent review. At final completion both facades contain only assembly, transaction entries and compatible forwarding, with no business SQL or state machine. Collaborator instance dependencies are acyclic; public contracts and all path-specific transaction semantics are preserved. A green focused test is not full acceptance.
## Risks and open decisions
The accepted architectural direction has no outstanding product decision. The main implementation risks are hidden query side effects, shared receipt/resource validation, lazy construction cycles, moved transaction boundaries and source-audit blind spots. Resolve an unexpected dependency in the smallest affected slice and update both language versions before changing the design. Do not silently absorb an algorithm, SQL, storage or protocol redesign into a movement PR.
## Implemented dependency boundaries
The facade evaluates the same settings and catalog validation once, in the original order, then supplies the same values and dependency instances to final collaborator fields. Agent construction is Rows → Queries/EventJournal → Policies/Projection → Commands → Admissions/Lifecycle/TurnExecution → ExecutionOperations. Journal construction is Rows → Fencing → WriterLifecycle/HistoryQueries/LifecycleAuthorization/ResourceWrites/ResourceReads → Commit. No collaborator receives a facade or a constructor callback bundle.
Shared Session/Turn point reads live in Rows; Queries forwards its three compatible point-read entries to those implementations. Policies owns shared admission predicates, while Rows owns their existing key encoders and database-time reads. The nine-argument default Session admission remains the original interface-default forwarding entry on the facade, preserving virtual dispatch and its transaction boundary. Resource row verification remains separate from object IO, leases and quarantine. Commit calls the extracted lifecycle-dispatch authorization block at its original location, with the existing call-local resource reader.
Session construction now resolves all nine former setter references. The required Spring dependencies remain required; standalone constructors retain their original null/in-process/unbound defaults. Either missing lifecycle dependency disables both. Only a same-data-source JDBC runtime binding repository enables CSI. Maintenance entry points and fixtures supply dependencies before constructing downstream consumers; actual Spring factory construction is covered separately.
The lock audit keeps its facade populations and pins the 23 remaining migrated private-helper overloads by owner/name/arity after deleting the unused `beginLifecycle/9`. Companion sources are analyzed with their complete family and retain separate, explicit SQL-owner boundary populations. Compiled-member coverage, fail-closed receiver resolution and the original negative regressions remain required. The original-line inventories below refer to the recorded baseline, not the new source locations.
## Agent entry inventory
Source: `packages/managed-agent-server/persistence/src/main/java/com/alibaba/qwen/code/managedagent/persistence/ManagedAgentStore.java`.
Transaction legend: **T** = exact plain `@Transactional` (default REQUIRED, DEFAULT isolation, readOnly false), facade class retains `@Qualifier("managedAgentTransactionManager")`; **M** = same with explicit MANDATORY; **RR** = explicit transactionManager `managedAgentTransactionManager`, readOnly true, REPEATABLE_READ; **N** = no declarative transaction on the current entry. N is not a promise of read-only behavior or permission to add a transaction. The worktree rows separately name their existing verification/quarantine transaction.
All overloads are listed by their source lines. Two constructors remain facade assembly; public static `readBinding` stays available at the facade and may forward to rows. Typed production callers below were cross-checked against actual receiver declarations, with the CSI local `var` construction path checked separately; an empty entry is explicitly marked rather than implying that the method can be removed. Tests remain consumers even where production does not currently call a compatibility entry.
### A1 AgentSessionQueries
| Entry / overload lines | Transaction | Direct production callers |
| ------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `findChildWorkspaceCwd` (691) | N | ManagedAgentService |
| `findChildLineage` (980) | N | ManagedAgentService, ManagedUserMessagePolicy, QwenHostedHarnessConnector |
| `hasCompletedWorkspaceClose` (2283) | N | No external direct production call found; preserve interface/internal/test usage |
| `completedWorkspaceCloses` (2290) | N | ManagedAgentService |
| `hasExecutionRegistryFacts` (3911) | N | ManagedActionStore, ManagedUserMessagePolicy |
| `sessionsWithExecutionRegistryFacts` (3919) | N | ManagedAgentService |
| `findSession` (4544) | N | ManagedAutomationService, WorkspaceCsiRuntimeAccess |
| `findSessionById` (4556) | N | ManagedRuntimeCoordinator, WorkspaceRuntimeResolver |
| `findLatestLifecycleOperations` (4648) | N | ManagedAgentService |
| `findTurn` (4674) | N | HarnessCoordinator, ManagedAgentService, QwenHostedHarnessConnector, SessionRecoveryService |
| `findActiveTurns` (4684) | N | ManagedAgentService, QwenHostedHarnessConnector |
| `findLatestTurns` (4708) | N | ManagedAgentService, SessionRecoveryService |
| `findModelContexts` (4747) | N | ManagedAgentService |
| `modelOptions` (4753) | N | ManagedAgentService |
| `findModelSelections` (4756) | N | ManagedAgentService |
| `findTurnModelSelection` (4776) | N | QwenHostedHarnessConnector |
| `findApprovalSelections` (4862) | N | ManagedAgentService, QwenHostedHarnessConnector |
| `findApprovalOwners` (4881) | N | ManagedAgentService |
| `findTurnApprovalSelection` (4895) | N | QwenHostedHarnessConnector |
| `listTurns` (5086) | N | ManagedAgentService |
| `findTurnSummary` (5110) | N | ManagedAgentService |
| `findLastVisibleStatusBeforeDelete` (6634) | N | ManagedAgentService |
| `requireSession` (6647) | N | ChildWorkspaceService, HarnessCoordinator, ManagedAgentService, ManagedExtensionCommandVerifier, ManagedExtensionRecordStore, ManagedRuntimeCoordinator, ManagedSessionStore, ManagedUserMessagePolicy, QwenHostedHarnessConnector, SessionLifecycleCoordinator, SessionLifecycleService, SessionRenameCoordinator |
### A2 AgentEventJournal
| Entry / overload lines | Transaction | Direct production callers |
| --------------------------------------- | ----------- | -------------------------------------------------------------------------------- |
| `findEvents` (5122) | N | ManagedAgentService |
| `findLatestEnvironmentEvents` (5140) | N | ManagedAgentService |
| `findControlEvents` (5184) | N | No external direct production call found; preserve interface/internal/test usage |
| `findTranscriptEvents` (5199) | N | No external direct production call found; preserve interface/internal/test usage |
| `appendPublicEventIfAbsent` (6601) | T | HarnessCoordinator, ManagedActionStore, ManagedToolResultStore |
| `appendLiveSessionEventIfAbsent` (6612) | T | No external direct production call found; preserve interface/internal/test usage |
### A3 AgentMessageProjection
| Entry / overload lines | Transaction | Direct production callers |
| ------------------------------------ | ----------- | ------------------------- |
| `findMessagePage` (5234, 5242) | RR | ManagedAgentService |
| `findMessageCoveredSequences` (5413) | N | ManagedAgentService |
| `findReplayWindow` (5435) | N | ManagedAgentService |
| `findReplayFloorTargets` (5454) | N | ReplayFloorAdvancer |
| `advanceReplayFloor` (5475) | T | ReplayFloorAdvancer |
| `findMaterializationTargets` (5506) | N | MessageMaterializer |
| `materializeNextBatch` (5516) | T | MessageMaterializer |
| `deferMaterializationTarget` (5620) | T | MessageMaterializer |
### A4 AgentSessionPolicies
| Entry / overload lines | Transaction | Direct production callers |
| ------------------------------------------------ | ----------- | --------------------------------- |
| `workspaceSessionOptions` (1096) | T | ManagedAgentService |
| `supportsPlanMode` (1113) | N | ManagedAgentService |
| `hasMcpServers` (1169) | N | SessionLifecycleCoordinator |
| `requireMcpManagement` (1180) | T | ManagedMcpManagementService |
| `requireMcpSettingsCommit` (1215) | M | ManagedExtensionRecordStore |
| `requireExtensionManagement` (1228) | T | ManagedExtensionManagementService |
| `requireConfigurationCommit` (1266) | M | ManagedExtensionRecordStore |
| `workspaceFilesEnabled` (2249) | N | ManagedAgentService |
| `workspaceShellEnabled` (2254) | N | ManagedAgentService |
| `changeSessionModel` (4790) | T | ManagedAgentService |
| `changeSessionApproval` (4926, 4935, 4944, 4954) | T | ManagedAgentService |
### A5 AgentSessionCommands
| Entry / overload lines | Transaction | Direct production callers |
| ------------------------------------ | ----------- | -------------------------------------------------------------------------------- |
| `insertTurnCommand` (1514, 1525) | T | ManagedAgentService |
| `insertCancelCommand` (1620, 1629) | T | ManagedAgentService |
| `beginSessionRename` (1685, 1693) | T | ManagedAgentService |
| `completeSessionRename` (1833, 1840) | T | ManagedAgentService, SessionRenameCoordinator |
| `abandonSessionRename` (1918) | T | ManagedAgentService, SessionRenameCoordinator |
| `deliverableRenames` (1933) | T | SessionRenameCoordinator |
| `claimRename` (1953) | T | ManagedAgentService, SessionRenameCoordinator |
| `markRenameSubmitted` (1979) | N | ManagedAgentService, SessionRenameCoordinator |
| `retryRename` (1997) | T | ManagedAgentService, SessionRenameCoordinator |
| `beginSessionMutation` (2080, 2089) | T | ManagedAgentService |
| `completeSessionMutation` (2151) | T | ManagedAgentService |
| `abandonSessionMutation` (2225) | T | No external direct production call found; preserve interface/internal/test usage |
| `replayCommand` (4502) | N | ManagedAgentService |
| `findCommand` (4524) | N | ManagedAgentService |
### A6 AgentSessionAdmissions
| Entry / overload lines | Transaction | Direct production callers |
| -------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------- |
| `insertSessionCommand` (388, 7651) | T | ManagedAgentService |
| `insertWorkspaceSessionCommand` (402) | T | ManagedAgentService |
| `insertInternalTeamLeadSessionCommand` (528) | T | No external direct production call found; preserve interface/internal/test usage |
| `findWorkspaceCreateReplay` (555) | N | ManagedAgentService |
| `replayWorkspaceSessionCommand` (570) | T | ManagedAgentService |
| `insertChildSessionCommand` (667, 677) | T | ManagedAgentService |
| `insertPublicChildSessionCommand` (703) | T | ManagedAgentService |
| `replayChildSessionCommand` (952) | T | ManagedAgentService |
| `insertCsiSessionCommand` (582) | N (requires ambient transaction) | WorkspaceCsiSessionCommands |
| `requireCsiRequest` (633) | N (locking bridge; caller transaction) | WorkspaceCsiRuntimeAccess, WorkspaceCsiSessionCommands |
### A6 -> AgentSessionQueries: compatibility display paths
These entries remain unchanged in the facade through A1-A5. A6 moves them into the existing AgentSessionQueries component while integrating worktree collaboration. They retain side effects delegated to the existing SessionWorktreeStore; no additional worktree wrapper is introduced.
| Entry / overload lines | Transaction | Direct production callers |
| --------------------------------- | --------------------------------------------------------- | --------------------------------------- |
| `findSessionWorktree` (157) | N + existing worktree verification/quarantine transaction | HarnessCoordinator, ManagedAgentService |
| `findSessionWorktrees` (162) | N + existing worktree verification/quarantine transaction | ManagedAgentService |
| `listSessions` (4564, 4569, 4574) | N + existing worktree verification/quarantine transaction | ManagedAgentService |
### A7 AgentLifecycleOperations
| Entry / overload lines | Transaction | Direct production callers |
| -------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `beginOperation` (2238) | T | SessionLifecycleService |
| `beginWorkspaceClose` (2260) | T | No external direct production call found; preserve interface/internal/test usage |
| `beginWorkspaceLifecycle` (2268, 2277) | T | SessionLifecycleService |
| `unarchiveWorkspaceSession` (2322) | T | ManagedAgentService |
| `beginChildClose` (2643) | T | ChildLifecycleAdmissions |
| `findOperation` (4062) | N | ManagedActionStore, ManagedAgentService, SessionLifecycleService, TaskCancelCoordinator, TurnInputEvidence |
| `findDeliverableOperations` (4070) | N | SessionLifecycleCoordinator |
| `claimOperation` (4108) | T | ActionResponseCoordinator, SessionLifecycleCoordinator, TaskCancelCoordinator |
| `completeOperation` (4143) | T | SessionLifecycleCoordinator |
| `failOperation` (4243) | T | SessionLifecycleCoordinator |
| `renewLifecycleOperation` (4308) | T | ActionResponseCoordinator, SessionLifecycleCoordinator, TaskCancelCoordinator |
| `blockLifecycleOperation` (4363, 4371) | T | SessionLifecycleCoordinator |
| `retryOperation` (4418, 4427) | T | ActionResponseCoordinator, SessionLifecycleCoordinator, TaskCancelCoordinator |
| `markOperationAnswered` (4473) | T | ActionResponseCoordinator |
### A8 AgentExecutionOperations
| Entry / overload lines | Transaction | Direct production callers |
| ------------------------------------------- | ----------- | -------------------------------------------------------------------------------- |
| `beginContextCompression` (2930) | T | ContextCompressionService |
| `contextCompressionState` (3005) | N | No external direct production call found; preserve interface/internal/test usage |
| `contextCompressionStates` (3020) | N | ManagedAgentService |
| `contextCompressionRequest` (3169) | N | ContextCompressionCoordinator, ContextCompressionService |
| `findContextCompressions` (3177) | N | ContextCompressionCoordinator |
| `claimContextCompression` (3186) | T | ContextCompressionCoordinator |
| `requireContextCompressionCommit` (3200) | N | ManagedExtensionRecordStore |
| `markContextCompressionSubmitted` (3217) | T | ContextCompressionCoordinator |
| `settleContextCompression` (3231) | T | ContextCompressionCoordinator |
| `retryContextCompressionPreparation` (3251) | T | ContextCompressionCoordinator |
| `awaitContextCompression` (3263) | T | ContextCompressionCoordinator |
| `completeCwdChangeOperation` (3289) | T | SessionLifecycleCoordinator |
| `failCwdChangeOperation` (3406) | T | SessionLifecycleCoordinator |
| `beginTaskCancelOperation` (3434) | T | ManagedTaskCancelService |
| `findDeliverableTaskCancels` (3539) | N | TaskCancelCoordinator |
| `findParkedOperations` (3564) | N | OperationReconciler |
| `settleParkedOperation` (3582) | T | AcceptanceEvidence, OperationReconciler |
| `turnInputAcceptance` (3641) | N | TurnInputEvidence |
| `renameAcceptance` (3703) | N | RenameEvidence |
| `settleParkedRename` (3733) | T | RenameEvidence |
| `settleTaskCancel` (3804) | T | TaskCancelCoordinator |
### A9 AgentTurnExecution
| Entry / overload lines | Transaction | Direct production callers |
| --------------------------------------- | ----------- | ------------------------- |
| `findDispatchable` (5627) | N | HarnessCoordinator |
| `findRuntimeRecoveryParked` (5639) | N | HarnessCoordinator |
| `claimTurn` (5654) | T | HarnessCoordinator |
| `renewTurn` (5675) | T | HarnessCoordinator |
| `releaseTurnLease` (5685) | T | HarnessCoordinator |
| `markTurnRecoveryBlocked` (5698) | T | HarnessCoordinator |
| `markTurnFailureRecovery` (5721) | T | HarnessCoordinator |
| `requestRecoveryRecheck` (5739) | T | HarnessCoordinator |
| `parkTurnForRuntimeRecovery` (5759) | T | HarnessCoordinator |
| `deferTurnRetry` (5779) | T | HarnessCoordinator |
| `scheduleTurnRetry` (5786) | T | HarnessCoordinator |
| `bindHarness` (5804) | T | HarnessCoordinator |
| `bindRecoveredHarness` (5831) | T | HarnessCoordinator |
| `markSubmissionAttempted` (5862) | T | HarnessCoordinator |
| `withdrawSubmissionAttempted` (5887) | T | HarnessCoordinator |
| `recordAdmission` (5904) | T | HarnessCoordinator |
| `recordRecoveryAdmission` (5948) | T | HarnessCoordinator |
| `retractContinuationOutput` (6009) | T | HarnessCoordinator |
| `retractHarnessTurnOutput` (6079, 6087) | T | HarnessCoordinator |
| `recordHarnessEvents` (6304) | T | HarnessCoordinator |
| `cancelUnadmittedTurn` (6461) | T | HarnessCoordinator |
| `cancelBeforeAdmission` (6505) | T | HarnessCoordinator |
| `failTurn` (6535) | T | HarnessCoordinator |
| `failWorktreeInitialTurns` (6541) | T | SessionWorktreeService |
| `failRecoveryBlockedTurn` (6562) | T | HarnessCoordinator |
### Rows compatibility forwarding
| Entry / overload lines | Transaction | Direct production callers |
| ---------------------- | ----------- | ------------------------------------------------------------------- |
| `readBinding` (7569) | N | WorkspaceFileStore, WorkspaceMigrationMain, WorkspaceMigrationStore |
Inventory covers **136 distinct entry names / 153 declarations**, including two package-private CSI bridge methods and one public static mapper, excluding two constructors.
### Worktree display decision and side effects
The worktree display entries are NOT pure reads. `findSessionWorktree` / `findSessionWorktrees` call `SessionWorktreeStore.descriptor(s)` (107-145), and `listSessions` calls `descriptors` after its batched page SQL (4645). `verifiedDescriptor` (112-120) verifies filesystem identity and reads the current branch; on `ManagedAgentException` it calls `blockIdentity` (368-378), which opens an EXISTING `TransactionTemplate` with `PROPAGATION_REQUIRES_NEW`, changes READY to BLOCKED and increments revision. The separate transaction deliberately survives a caller's failed admission rollback. Final decision: exclude these three entry names from the first A1 pure-query slice and retain their original facade implementation until A6. In A6 they move to the existing AgentSessionQueries component as explicitly side-effecting compatibility display paths, reusing SessionWorktreeStore validation/quarantine and its existing transaction. Do not add readOnly, omit verification, copy quarantine code, remove the existing transaction, or add another worktree wrapper.
The same existing worktree verification path appears in policy/execution admission through `requireCwdChangeRegistryFacts` (3894): `SessionWorktreeStore.requireReady` then `hasExecutionRegistryFacts`. Preserve worktree verification before the creator/binding facts check, including its persisted quarantine semantics.
### Transaction and bridge exceptions
- `findWorkspaceCreateReplay` (555) has no declaration; `replayWorkspaceSessionCommand` (570) has plain `@Transactional`. Both re-check replay digest, internal-team-lead identity, workspace binding and current read permission. Do not normalize the two transaction modes.
- `insertCsiSessionCommand` (582) is package-private, has no transaction annotation, explicitly requires an active transaction, and rejects journal event transport. `requireCsiRequest` (633) preserves the same transport isolation, locks Session plus runtime-request key and validates the CSI profile/request identity. The caller `WorkspaceCsiSessionCommands.create` (19-47) rejects an ambient transaction, starts a fresh 10-second TransactionTemplate, checks its original physical transaction connection, locks placement then retention, verifies registration, then calls both Store bridges on that connection. This caller uses the standalone `var sessions = new ManagedAgentStore(...)` construction path.
- `requireCsiRequest` is also called by `WorkspaceCsiRuntimeAccess.request` (183-190); preserve all pre-existing continuation/retention lock behavior in that caller. Keep its facade method/package visibility and the seven-argument standalone constructor available.
- `readBinding` (7569) is public static and directly used by `WorkspaceMigrationMain`, `WorkspaceMigrationStore` and `WorkspaceFileStore`; keep a facade forwarding entry even after moving actual mapping into rows.
- `requireMcpSettingsCommit` (1215) and `requireConfigurationCommit` (1266) are MANDATORY. `requireContextCompressionCommit` (3200) has NO annotation; it is called from `ManagedExtensionRecordStore` in an existing commit flow, and must not acquire an invented MANDATORY/REQUIRES_NEW wrapper.
- `findMessagePage` overloads (5234/5242) explicitly use managedAgentTransactionManager, readOnly true and REPEATABLE_READ. Ordinary Query entries are unannotated and must remain so.
- `deliverableRenames` (1933) is a transactionally mutating scan: it parks expired SUBMITTED claims and resets expired PREPARING ones before selecting work. `markRenameSubmitted` (1979) is a guarded write with no declarative transaction. Keep both in Commands and preserve the difference.
- `contextCompressionState` (3005) calls `ensureCompactionProjection` (3066); a missing projection opens a default TransactionTemplate with a DataSourceTransactionManager and replays journal rows. Batch `contextCompressionStates` (3020) may fall back to this write path and marks unavailable states. Both belong in A8, with the same programmatic transaction, rather than A1.
- `publishAfterCommit` (7489) publishes only after synchronization commit; if no synchronization exists it publishes immediately. Empty events do nothing. The EventJournal extraction must retain the standalone fallback and avoid replay/rollback notifications.
### Actual lock families (do not replace them with one global order)
- Row helper `requireSessionForUpdate` (7061/7066) locks the PUBLIC Session row first and reads/locks its event-stream counters only AFTER that lock. The list mapper and the strict single-session mapper have different missing-stream behavior. `requireTurnForUpdate` (7106) then locks a Turn where that path needs both; its NOT_FOUND mapping must survive extraction.
- Many admission/mutation/policy entrypoints begin `WorkspaceMigrationAdmission.lockTenant`, then Session/stream, then Turn/receipt/operation as required. MCP/extension idle rechecks (1340) specifically use fresh locking reads in this order: Session plan -> active Turn -> unsettled child-run record -> requested action records -> active operation/rename/receipt. Existing journal snapshots are the reason these may not become ordinary consistent reads.
- Lifecycle admission `beginLifecycle` (2695) locks migration tenant, conditionally retention tenant, then Session/stream; it preserves failed-operation replay/resume, authorizes state, checks idle journal, writes operation/fence/head and event in original order. `completeOperation` (4143) first applies the legacy CSI guard, then conditionally placement and bounded retention, then deletion lock either before or after Session according to lifecycle protocol, then operation claim. Do not normalize its branches.
- `ManagedLegacySessionGuard.requireLegacyMutation` itself performs the indexed CSI root locking read through `ManagedSessionStore.namedRootDeadlock`, with a 10-second query timeout. Some Turn lease paths use this guard plus a guarded UPDATE, while admission/terminal paths take a full Session/stream lock and then Turn. Retain that distinction; a generic row-helper transaction/lock would change semantics.
- Materialization and replay-floor advancement lock Session/stream before inspecting Turn evidence or updating coverage. Retraction repair `rebuildTurnText` (6172) locks stream coverage and message rows only after the enclosing Turn workflow has acquired its existing owner/epoch locks.
### A1 dependency closure and boundaries
The A1 ordinary subset needs JdbcTemplate, configured ObjectMapper, HarnessRules (approval-rule decoding), the construction-time immutable modelCatalog snapshot and Session/Turn mappers. WorktreeFiles and the three worktree display entries are deferred to the A6 -> AgentSessionQueries delivery. Shared row work is `readSession`, sessionMapper/sessionListMapper, turnMapper/turnSummaryMapper, readBinding, nullable/additive column helpers, StreamCounters and the existing locked Session/Turn readers; move each only when first actually needed, preserving strict-vs-list stream behavior and all schema projections. JSON decoding is only shared where a concrete row mapper needs it, not by introducing a generic Support/Context object.
`findLatestTurns` selects by last turn.accepted event rather than latest gmt_create. The A6 `listSessions` migration must preserve tenant/actor grants, archive/title/cursor filters, limit+1 semantics, batched worktree metadata and validation/quarantine; keep query counts, ordering and byte representation. `findSessionById` is an existing cross-tenant lookup for runtime resolution; do not silently add tenant filtering or expose it as a new HTTP operation.
Point read/display views can live in Queries; operation WORK SCANS remain with their state machine (rename -> Commands, materialization/replay floor -> Projection, compression/task cancel/parked -> ExecutionOperations, lifecycle dispatch -> LifecycleOperations, Turn dispatch/recovery -> TurnExecution). Read-only Session busy predicates shared across policies/commands/lifecycle should be given one narrow owner when first migrated; do not force a component to call the facade simply to reuse them.
### Concrete one-way seams for later slices
- A2 EventJournal owns appendEvent/appendEvents, event identity/source-key handling, eventMapper and after-commit publishing; it can depend on Rows, not on workflow components. It does not perform message materialization synchronously.
- A3 MessageProjection owns projection scan/materialization/replay-floor state and the `rebuildTurnText` repair helper. Its dependency on event reads is one-way toward EventJournal/Rows. Public `retractContinuationOutput` (6009) and both `retractHarnessTurnOutput` overloads (6079/6087) remain A9 workflow entries: owner/lease/epoch checks and event/identity deletion happen before A3 repair, in the same facade transaction. Do not give Projection a callback to TurnExecution or to the facade.
- A4 policy transactions have their own existing actor-scoped SET_MODEL/SET_APPROVAL replay protocol, using ManagedReceipts directly (4790 onward, 4954 onward). They do not call generic insertCommand/settleTurnCommand. Preserve those scopes and existing result JSON rather than rewriting them onto a new generic receipt flow. A4 may use Queries/Rows/EventJournal and existing definitions/skills; later Commands/Admissions use the policy checks in one direction.
- A5 Commands owns common `insertCommand` (7156/7163/7170), `settleTurnCommands` (2477), `settleTurnCommand` (2532), turn command operation inserts (2416), command replay and receipts. A9 recordAdmission/recordRecoveryAdmission/recordHarnessEvents and terminal paths call this settlement seam. A8 accepted-outcome reconciliation must use the same command/rename settlement semantics; no reverse callback from Commands to A8/A9.
- A6 Admissions depends on lower policy/command/event/row components plus existing definitions/skills/worktree components. It must preserve replay-before-new-resolution timing, creator/child scope, definition pinning and rollback behavior.
- A7 LifecycleOperations owns operation point reads, replay/claim/lease/retry/completion and the operation mapper. Current shared APIs are used by SessionLifecycleCoordinator plus ActionResponseCoordinator and TaskCancelCoordinator. Keep their facade routing stable. A8 calls A7's operation lookup/common claim primitives where needed: for example claimContextCompression (3186) writes its specialized CAS then reads findOperation; requireContextCompressionCommit (3200) checks the same operation through that lookup. The coordinator already chooses whether to call generic completion or specialized cwd/task-cancel settlement; A7 must NOT become a dispatcher calling A8/A9.
- A8 ExecutionOperations owns specialized cwd/compaction/task-cancel/unknown-acceptance state machines and their scans, with downward dependencies on Rows/Queries/Policies/Commands/EventJournal and A7's shared operation primitives. Generic claim/retry does not acquire a callback for specialized outcomes.
- A9 TurnExecution needs Rows/Queries, common Commands settlement, EventJournal and Projection repair. It has no reason to depend on LifecycleOperations merely because coordinators separately call lifecycle APIs. Preserve that separation rather than installing a cycle.
- Acyclic instance construction is an explicit acceptance test for each extraction, not a claim proven by these broad labels. In particular, shared predicate helpers such as hasOpenOperation/hasOpenExecutionOperation/hasPendingRenameDelivery and existing schema key encoders must not produce Policies -> Commands -> Policies or Commands -> Lifecycle -> Commands cycles; migrate the actual dependency closure and use the existing schema/receipt helpers directly where appropriate. No constructor function bundle, facade callback or mutable setter is needed.
## Journal entry inventory
Baseline: `e3c7a6d7efc6df6c58ffa8ddd9c42dbb0e1784c4` (2026-11-02). `M:<line>` below refers to `packages/managed-agent-server/persistence/src/main/java/com/alibaba/qwen/code/managedagent/persistence/ManagedSessionStore.java`, which has 3,931 lines. Other unqualified class filenames refer to the same persistence package; the controller is in server and the Hosted connector is in adapters. This inventory defines migration boundaries; it does not establish that subsequent slices are implemented or accepted.
### 1. Decisions and boundaries
- The implementation has 27 public business signatures under 22 names, all declared by the existing contracts `repository.ManagedSessionStore`. It also has three constructors, eight assembly setters holding nine references, 17 package-private methods, two package-private records and one lock-timeout constant. All are accounted for below.
- This Store has no `MANDATORY` method. Do not assign the overall design's `MANDATORY` requirement to a Journal entry that does not have it. Journal's explicit transaction exceptions are the two `READ_COMMITTED` history queries and the `NOT_SUPPORTED` initial-journal check, plus resource reads and package bridges without declarative transactions.
- New internal objects must not call the facade. Resource-row integrity checks and activation-row mapping needed by B1 move into `JournalStoreRows` with their first actual consumers, so B1 requires neither future B3/B5 components nor facade callbacks. Object reads, leases, verification-time updates and quarantine remain outside Rows.
- Preserve the public business interface, package entry points, `PublicationWriter`, `TerminalCandidate`, static lock helpers and constant on the facade. Exposing internal helpers within the package supports explicit typed dependencies; it must not introduce a generic Context or Support object.
- Preserve path-specific lock sequences. Ordinary heartbeats do not acquire tenant guards; CSI continuations lock the root before placement/retention guards; publication and snapshot paths have their own preceding legacy probes and caller-held locks. Do not replace these paths with one enforced global sequence.
### 2. Transaction legend
`T`: the original method has `@Transactional`. Preserve manager resolution through the class-level `@Qualifier("managedAgentTransactionManager")` (M:82); the method annotation does not name a manager explicitly. Propagation is `REQUIRED`, isolation `DEFAULT`, `readOnly=false`, with default timeout and rollback rules. Preserve the annotation on each overload; an existing self-call does not create another proxy boundary.
`RO`: the same manager, `REQUIRED` / `DEFAULT` / `readOnly=true`.
`RC`: the same manager, `REQUIRED` / `READ_COMMITTED` / `readOnly=true`. Internal calls to `restore` are currently self-calls and must not acquire a new transaction after extraction.
`NS`: the same manager, `NOT_SUPPORTED` / `DEFAULT` / `readOnly=true`; suspend an ambient transaction without opening a new one.
`N`: no declarative transaction. Extraction must not add a proxy or annotation. An existing caller determines whether a transaction is active. Standalone construction never automatically applies Spring method transactions.
`P`: an `N` outer method with the programmatic owner-read flow described in section 6.
Lock traces below retain existing order and branches. Repeated acquisition of an already-held lock is abbreviated, not authorized for removal. In lock columns, `P` means the placement tenant guard; `Tg` the output-publication retention tenant guard; `R` the session root; `H` the managed-session head; `D` the harness drain head; and `C` the CSI binding/slot/retirement/progress lock closure on the original connection. Bounded tenant waits remain five seconds. `namedRootDeadlock` continues to translate only its existing deadlock cases.
### 3. Complete public business entry matrix
| Original entry and location | Owner | Transaction | Existing principal locks and effects | Production callers |
| ---------------------------------------------------- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `authorizeLifecycle` M:211 | B2 JournalLifecycleAuthorization | T | Credential → P → Tg → ensure R/event stream → placement → session row → lifecycle claim → H. With a kind, verify a live writer; without a kind, the DRAINING branch verifies identity/state under its existing rules. | ManagedSessionStoreController:102 |
| `authorizeOrdinary` M:394 | B2 | T | Credential → R → placement → ordinary claim/drain admission, including the legacy-close exception → config checks → H → writer. Do not add tenant guards. | Controller:63,93 |
| `approvalSelection` M:425 | B2 | T | Ordinary authorization followed by `ma_session FOR UPDATE`; returns model/approval/permission selection. This is not a pure query. | Controller:72 |
| `acquireWriter` M:629,635, four/five arguments | B1 JournalWriterLifecycle | T/T | The lifecycle-authority branch first takes P→Tg→R→claim. The common path takes P→Tg→CSI/R→physical-open→ensure R/event stream→placement/control storage→materialize D→claim/admission→session row→H. Preserve create, renewal and generation changes; cold CSI takeover uses binding and history proof. | Controller:113; manually assembled journal fixtures also exercise these overloads |
| `renewWriter` M:807,813, four/five arguments | B1 | T/T | Preserve authority guards, then placement→CSI continuation, which is R for ordinary sessions and R→P→Tg→C for pinned sessions→H. Ordinary heartbeat does not add Tg. Recheck writer and CSI leases; activation renewal also updates the journal transaction/head. | Controller:124 |
| `sealWriter` M:891,897, four/five arguments | B1 | T/T | Authority guards→placement→lockCsiOriginal, which rejects ordinary sealing of private CSI→R/H. An identical sealed identity replays early. Writer expiration is allowed on this path; identity replacement is not. | Controller:135 |
| `quiesceOwnership` M:938 | B1 | T | Credential placement identity→current placement→R/H→writer→update session to QUIESCING. | Controller:152 |
| `releaseOwnership` M:952 | B1 | T | P→Tg→R→existing receipt/resource locking validation and early replay. A new release performs placement/claim→H→writer/proof→resource/record/ref→SEALED head→RELEASED session, in one transaction. | Controller:144 |
| `blockRecovery` M:1051 | B1 | T | Placement→CSI continuation/R→H. Writer/CSI-current checks precede replay and repeat before the recovery update. | Controller:162 |
| `commit` M:1112,1118, four/five arguments | B6 JournalCommit | T/T | Authority guards→P→placement→CSI/R/C/optional H→command lookup. Duplicates return early. New commits perform physical-open→R/H→resource/projection/journal/head in one transaction; see section 7. | Controller:173; ToolPublicationAdmissionStore:397,411 |
| `restore` M:1485 | B3 JournalHistoryQueries | RO | Plain head read→scope/storage-policy/read grant→activation projection; do not add FOR UPDATE. | Controller:188; ToolPublicationAdmissionStore:282; ToolPublicationDataStore:950,2229; ManagedNativeJournalSnapshots:205,237,249,255,291 |
| `transactions` M:1560 | B3 | RO | Plain head/read grant→revision/byte-size selection→bounded payload read. Preserve continuity, integrity and workspace checks. | Controller:209; ManagedNativeJournalSnapshots:129,131,132; ToolPublicationStore:559,561,562,566,587 |
| `recentTransactions` M:1644 | B3 | RC | Initial restore/prefix validation→two-stage reverse pagination→another restore before return. Do not change to repeatable read. | Controller:221 |
| `commandTransaction` M:1700 | B3 | RC | Initial restore/prefix→command hash/size/row→another restore before return. | Controller:232 |
| `hasPristineInitialJournal` M:1759 | B3 | NS | Validate the complete initial journal/header/resources and require equal initial/final heads. Do not acquire a writer or change fencing; preserve exception-to-false behavior. | SessionRecoveryService:123 |
| `readResource` M:1920 | B5 JournalResourceReads | N | Credential/head/read grant→retention read lease→resource FOR UPDATE→metadata/digest verification→last_verified_at update→optional object IO/quarantine→lease check/close. Do not move into HistoryQueries. | ManagedToolResultProjector:176,318; ToolPublicationStore:630; ChildResultRelay:469,474,568,573,1161,1202,1212; SessionLifecycleCoordinator:937 |
| `readOwnerResource` M:1968 | B5 | P | Reject an ambient transaction. Non-CSI falls back to readResource. CSI uses short admission transaction→fetch on an independent connection→short revalidation/receipt/lease completion transaction; see section 6. | Controller:269 |
| `publishAgentInstructions` M:2328 | B4 JournalResourceWrites | T | Scope→legacy CSI index locking probe→resource FOR UPDATE/insert or idempotent validation. Do not introduce writer-lease requirements. | QwenHostedHarnessConnector:2111 |
| `publishAgentSkills` M:2341 | B4 | T | The same path; retain the original AgentDefinitionExtensions.skillsBytes encoding. | QwenHostedHarnessConnector:2121,2145 |
| `publishInheritedExtensionResource` M:2350 | B4 | T | Reject snapshot resources→legacy probe→kind/schema/bytes/digest→resource FOR UPDATE/insert or idempotent validation. | QwenHostedHarnessConnector:2044,2049,2052 |
| `publishToolResult` M:2410,2416, four/five arguments | B4 | T/T | Authority guards→byte validation→physical P→Tg→placement→CSI continuation/R→H→no-lifecycle-claim, including v2 rejection even with authority→writer/native head→resource→current fence→insert/replay→CSI time. | Controller:198 |
| `hasLiveWriter` M:3459 | B1 | T | Direct H FOR UPDATE, then database time. Do not add root/tenant locks or change this to read-only. | SessionLifecycleCoordinator:602,815,846 |
Caller discovery excludes similarly named calls that do not target this Store: QwenHostedHarnessConnector:273 calls the SDK client's releaseOwnership, and :889 calls a builder's approvalSelection. ToolPublicationAdmissionStore:295,342,361 and ToolPublicationStore:493 call `data.readResource`, not this Store.
### 4. Complete package compatibility entry matrix
| Original entry | Final implementation owner | Transaction/connection contract | Callers and preserved details |
| ---------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `usesDataSource` M:522 | Facade or identity-preserving Rows delegate | N; `source != null && jdbc.getDataSource() == source` | ToolPublicationDataStore:96–99, ToolPublicationAdmissionStore:53–56, WorkspaceCsiApplicationHistory:60–61. Do not replace identity with equals, unwrapping or matching connection-pool URLs. |
| `lockSnapshotWriter` M:577 | B1 WriterLifecycle | N; caller already owns the transaction | ManagedNativeJournalSnapshots:89,108,179,200. Preserve P→lockPublicationWriter→ordinary admission→H→restore mapping and the existing PublicationWriter/RestoreHead value semantics. |
| `lockPublicationWriter` M:598 | B1 WriterLifecycle | N; caller already owns the transaction | ToolPublicationStore:327; ToolPublicationDataStore:1121,1181; ToolPublicationAdmissionStore:374. Legacy index probe first, then physical P→Tg→placement→CSI/R→H→writer/native head. Account for locks already held by each caller. |
| `readSnapshotResource` M:1925 | B5 ResourceReads | N; snapshots already supply an isolated programmatic transaction | ManagedNativeJournalSnapshots:520. updateVerification=false means plain resource SELECT, no last_verified_at update and no quarantine; retain retention-lease acquisition/check/close. |
| `requireCsiApplicationCredential` M:2581 | B1 Fencing | N; the authority's same physical connection must be bound to a transaction on this jdbc DataSource | WorkspaceCsiApplicationStore:105,119,137,184,213,230. Preserve authority.requireConnection, DataSourceUtils.isConnectionTransactional(target, same datasource), then credential→isolated journal→R/H→CSI/token. |
| `requireCsiHistoricalCredential` M:2601 | B1 Fencing | N; no SQL or locks; validate credential and retained token hash | WorkspaceCsiApplicationHistory:92, inside its existing read-only repeatable-read snapshot. Do not introduce live-writer or lease checks. |
| `preflightCsiTerminal` M:2609 | B6 Commit | N; same authority connection, PreCut, same-DataSource bindings | WorkspaceCsiApplicationStore:125,144. Strict bytes→credential/isolation→commit validation→R/H→writer/head→terminalPreflight(target). Return the existing TerminalCandidate type. |
| `commitCsiTerminal` M:2636 | B6 Commit | N; same authority connection, Stage.FROZEN | WorkspaceCsiApplicationStore:192. Frozen bytes/digest match→repeated preflight→resource fence→journal fence→SEALED-head CAS→final time check. Do not call ordinary commit or add transport notification. |
| `validateCsiTerminalBytes` M:2695 | B6 Commit static validation, forwarded by facade | N; no SQL or IO | WorkspaceCsiApplicationStore:336. Preserve strict JSON settings, exactBody and the shared validation rules. |
| `lockCsiOriginal` M:3162 | B1 Fencing | N; pinned CSI requires its original transactional connection; ordinary standalone may still return null | ToolPublicationAdmissionStore:371. Preserve its surrounding original-settled-result and tenant lock ordering. |
| `toolResultLimit` M:2507 | Rows' pure resource-row constraint, with facade static forwarding | N; no SQL or IO | Existing package static entry; fixed kind limits also used by verifyStoredResource. |
| `commandKeyHash` M:3832 | Rows' key encoding, with facade static forwarding | N; pure bytes | ManagedAgentStore:3689. Preserve operation + NUL + commandId, UTF-8 and SHA256. |
| `journalCorrupt` M:3861 | Rows' row-integrity error, with facade static forwarding | N | ToolPublicationStore:535,544,557,563. Preserve HTTP status, code and message. |
| `skipContended` M:301 | B1 Fencing static helper | Execute the caller-supplied TransactionTemplate; suppress only tenant_guard_contended | ManagedProjectionResourceCollector:42; SessionResourceCollectionCollector:188. Do not force REQUIRES_NEW. |
| `lockBounded` M:314 | B1 Fencing static helper | No transaction of its own; execute the existing Runnable | ManagedToolResultStore, ManagedProjectionResourceCollector, ToolPublicationDataStore/AdmissionStore/Store, SessionResourceCollectionCollector, ManagedAgentStore and ManagedUserMessageStore. |
| `lockBoundedResult` M:321 | B1 Fencing static helper | The same | ToolPublicationRetentionStore:156. Translate only PessimisticLockingFailureException / QueryTimeoutException to tenant_guard_contended. |
| `namedRootDeadlock` M:341 | B1 Fencing static helper | The same | ToolPublicationRetentionStore:112; ManagedLegacySessionGuard:14. Translate only DeadlockLoserDataAccessException or MySQL 1213 in the cause chain; preserve ordinary root-wait failures. |
Retain the facade's existing package records `PublicationWriter` (M:571) and `TerminalCandidate` (M:2577), and `GUARD_ACQUIRE_TIMEOUT_SECONDS = 5` (M:111). Existing external root/retention code continues to call the facade's static helpers. Static compatibility forwarding is not a new instance dependency or facade callback.
### 5. B0 assembly facts and migration requirements
| Original setter | References and current Spring condition | Standalone default | Constructor migration requirement |
| -------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `configureHarnessRules` M:87 | HarnessRules; required @Autowired | null | Required Spring parameter; retain standalone null instead of adding a SdkHarnessRules default. |
| `configureReaderVersion` M:92 | StorageReadPolicy; required @Autowired | StorageReadPolicy.inProcess() | Required Spring parameter; standalone remains inProcess. |
| `setLifecycleExecution` M:205 | WorkspaceExecutionStore + SessionQueryStore; one two-argument `@Autowired(required=false)` method | null/null | Previously, either missing bean skipped the entire setter. Resolve optional constructor parameters and normalize both to null if either is absent; do not partially enable the pair or fail startup. |
| `setPublicationObjects` M:527 | Optional ToolPublicationObjectStore | null | Optional construction parameter; do not create default object storage. |
| `setToolResults` M:532 | Optional ManagedToolResultStore | null | Optional construction parameter; preserve whether capture is enabled. |
| `setJournalEventTransport` M:539 | Optional JournalEventTransport; no bean when disabled | null | Optional construction parameter and byte-identical disabled behavior. CSI isolation continues to reject a non-null transport. |
| `setCredentials` M:546 | WriterCredentialPolicy; required @Autowired | WriterCredentialPolicy.unbound() | Missing Spring dependency still fails closed; standalone does not gain bound credentials. |
| `setCsiBindings` M:551 | Optional RuntimeBindingRepository | null | Retain only instanceof JdbcRuntimeBindingRepository with `usesDataSource(jdbc.getDataSource())`; mismatches still become null, without a new startup failure. |
All constructors and these eight setters originally have no declarative transaction (`N`). The three existing constructors at M:177/181/187 are `(JdbcTemplate)`, which creates extensionRecords; `(JdbcTemplate, extensionRecords)`, which uses actions=null; and Spring's `(qualified JdbcTemplate, extensionRecords, actions)`, whose three dependencies are required. Their constructor chain creates one DataSourceTransactionManager for the existing ToolPublicationRetentionStore and ownerReads template, with ownerReads timeout=10. Preserve both programmatic transaction objects and their configuration; do not replace them with new Spring beans or an additional global transaction-manager dependency.
There are only two production manual callers: `server/.../maintenance/WorkspaceCsiRuntimeMain:106–107`, using bare construction plus same-DataSource CSI bindings, and `WorkspaceCsiWorkerAckMain:53–54`, using bare construction plus SdkHarnessRules. Their defaults and use cases differ; do not replace both with a common fixture that enables additional dependencies.
The central manual-test path is `VerificationFixtures.configure(ManagedSessionStore):40–46`, which currently assigns only HarnessRules and StorageReadPolicy. B0 replaces it with an explicit construction factory, as with Agent wiring; do not mutate final fields reflectively, rebuild an already-created instance, or merely rename setters. The current scan covers 52 files and 150 construction points, consisting of two production and 148 verification calls, plus 16 explicit setter calls in 11 files. Check each constructor argument, original setter condition and fixture construction order during migration. Refresh the inventory during implementation: eliminating old assembly calls proves only their removal, not behavioral correctness.
No current explicit `@Bean` factory returning ManagedSessionStore was found. `ToolPublicationAsyncVerificationTest:317` does have `.withBean(ManagedSessionStore.class, () -> fixture.sessions)`, in a context that also supplies HarnessRules, StorageReadPolicy, credentials, bindings and object storage. Migrate the constructed fixture rather than relying on postprocessing to apply removed setters. Add an actual @Bean factory regression in B0 to prevent recurrence of #16061's missing-dependency failure.
Dependency inspection: ManagedToolResultStore:40 depends on EventProjectionStore, implemented by the Agent facade; WorkspaceExecutionStore:38 depends on jdbc, transaction manager, WorkspaceStorageGuard and settings; LiteTopicEventTransport:45 depends only on producer/provider/topic. These edges do not justify adding a new lazy cycle for B0. Preserve the existing Agent→records and skills→records lazy edges. Full Spring context, transaction-proxy and actual factory tests remain required.
### 6. Actual resource-read effects and transactions
#### Ordinary and snapshot reads
The shared implementation at M:1930 first validates scope, credential, resource identity, head and read grant. The read grant itself reads retention state and database time. `outputRetention.read` (ToolPublicationRetentionStore:379) uses its existing TransactionTemplate, with default REQUIRED/DEFAULT/readOnly=false, to execute Tg→R→H through lockGenericSession and insert a 120-second `ma_read_lease`. It joins an existing ambient transaction. The outer resource method has no NOT_SUPPORTED annotation; do not add one.
Ordinary reads use findResource with lock=true, verify the row, update last_verified_at (M:1953), and read an object stream for TOOL_PUBLICATION. Length/digest failures quarantine capture/resource state. Snapshot reads use lock=false, do not update verification time and do not quarantine, but still insert, conditionally renew, check and close retention leases. Neither belongs in a pure-query component. `verifyStoredResource` itself does not perform those effects; see section 8.
The snapshot caller's existing isolated template at `ManagedNativeJournalSnapshots:56–61` uses REQUIRES_NEW + READ_COMMITTED. Its admission/basis (:89), build (:92), lease close (:94) and resource read (:520) retain that template. **Preserve this existing REQUIRES_NEW behavior.** The prohibition on introducing REQUIRES_NEW forbids additional boundaries; it does not authorize deleting this one or copying it into the new components.
#### Owner reads
M:1978 rejects both `isActualTransactionActive()` and an already-bound jdbc DataSource resource. Non-CSI traffic at M:1995 still falls back to the ordinary reader. The CSI path remains:
1. The ownerReads template at M:197–200 uses the same-DataSource manager, REQUIRED/DEFAULT/readOnly=false and a ten-second timeout. Admission runs lockCsiContinuation→original admission or DRAINING checkpoint-continuation check→R/H→read grant→resource metadata FOR UPDATE. Require inline storage, construct identity evidence, recheck CSI/read lease, insert the OPEN CSI_RESOURCE_READ receipt, recheck again, insert ma_read_lease with generation=0, and retain the receipt.
2. After leaving that transaction, fetchNativeResource at M:2172 obtains a separate DataSource connection to SELECT the complete inline bytes with a ten-second statement timeout. This independent reading stage must not hold the admission transaction or reuse its physical connection.
3. finishNativeRead at M:2199 opens another short ownerReads transaction. Relock continuation/original→R/H→read receipt FOR UPDATE; changed identity/receipt is rejected. Unknown fetch failure records UNKNOWN / jdbc_fetch_unknown and retains the lease under the existing path. On a returned fetch, lock/check the read lease, lock resource metadata, compare row identity/bytes/hash, recheck CSI/read grant, update verification time, record RETURNED with verified or the refusal code, and delete the lease only after the final recheck. Existing early fencing failures roll back rather than forcing settlement; do not replace this with unconditional finally cleanup.
4. A RuntimeException/Error during fetch attempts completion and attaches completion failure as suppressed before rethrowing the original error. Do not wrap owner IO in a large transaction or collapse repeated current-time/read-grant checks.
### 7. Commit atomicity, CSI rechecks and notification
Ordinary/lifecycle commit at M:1118–1459 retains one complete entry transaction. Command replay at M:1176–1214 returns before new resource/projection writes. CSI replay first validates the current writer, activation replay and current time; ordinary replay still rejects a v2 lifecycle claim. Duplicates emit no event.
A new commit validates the head/generation/lease, performs native preflight when required for commitFileHistory, toolIntent or commitCheckpoint, then reads database time again to recheck the writer (M:1228–1245). `commitResources` at M:2809 executes its supplied writeFence after reading each existing resource and before writing. This is an existing call-scoped callback and may remain; it must not become a constructor-supplied collection of functions.
The same transaction then applies the declared-resource census, checkpoint/history/Hosted closure validation, HostedCleanupReferences, lifecycle-dispatch authorization at M:1313 using the paired execution/session dependencies, extensionRecords.apply/applyNative, CSI acceptCommit plus another writer check at M:1343–1367, and actions.apply. requireCsiWriterCurrent runs before journal INSERT (M:1378) and again before head UPDATE (M:1421), followed by tool-result capture and ManagedUserMessageStore.capture. Resource, projection, journal and head writes must not acquire separate commits.
`publishJournalAfterCommit` at M:1466 is called only at the end of a new commit (M:1455). Null transport does nothing. Without synchronization, standalone operation publishes immediately; with synchronization, register afterCommit. Rollbacks and replays do not notify. CSI continuation/terminal requires isolated journals through requireCsiJournalIsolation (M:3245), rejecting transport, so terminal commit has no notification. Do not add one to unify commit paths.
The three connection-aware CSI terminal bridges use the existing ten-second transaction created by `WorkspaceCsiApplicationStore.tx:402–410` and its physical connection's bounded proxy. Preserve the authority's connection identity, DataSourceUtils.getTargetConnection and isConnectionTransactional checks; a new connection, same-URL check or helper-owned transaction is not equivalent. Preflight also verifies same-DataSource JdbcRuntimeBindingRepository. commitCsiTerminal compares frozen bytes, repeats preflight, executes authority-time/writer fencing for every resource, fences again before journal INSERT and SEALED-head CAS, and checks authority time before returning. Preserve TerminalCandidate's request, proof and token-hash values.
### 8. Shared closure and one-way dependencies
`JournalStoreRows` is limited to existing row access, mapping, integrity checks required when reading rows, fixed key/byte constraints, and the explicitly listed narrow row initialization below. It is not purely read-only and must not become generic business Support.
| Original method | First actual consumer | SQL/effects | Migration decision |
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| headMapper/transactionMapper/resourceMapper and HeadRow/TransactionRow/ResourceRow | B1 writer heads and release receipts | Row mapping; no business-state writes. | Rows. |
| findHeadForUpdate/requireHeadForUpdate M:3119/3359; requireHeartbeatHead M:3140; requireHead M:3367; databaseNow M:3474 | B1 | Preserve R→H and the live read for the first pair; heartbeat locks only H; plain head takes no lock; databaseNow is SELECT. | Rows. A shared helper must neither add nor remove locks. |
| findResource M:3393/3397; requireResourceScope M:3787; requireTransactionWorkspace M:3798 | B1 release receipt | Default FOR UPDATE; only explicit false performs a plain read. Scope checks do not write. | Rows; preserve every path's lock argument. |
| verifyStoredResource M:3575 | B1 releaseOwnership M:997 | State/length/SHA256/metadata checks. TOOL_PUBLICATION uses SELECT COUNT to validate matching capture metadata; inline managed-tool-outcome uses SELECT COUNT to check quarantine. **No UPDATE, object IO, lease operation or quarantine write.** | Move with B1 into Rows' resource-row integrity boundary; B3/B4/B5/B6 reuse it without copies. |
| toolResultLimit M:2507 | verifyStoredResource at M:3618 | Pure kind-to-fixed-limit switch; no SQL or IO. | The same boundary, retaining the facade's static entry. |
| activationLeaseRow M:1512, ActivationLeaseRow and restoreHead M:1495 | B1 renewWriter M:863 and snapshot bridge M:591 | SELECT, strict JSON/digest and head/activation-projection consistency checks; no update, IO or lease operation. | Move into Rows with B1; public history entries move later in B3. |
| requireHeadScope M:3742 | B1 | Scope/state/counter/digest checks and StorageReadPolicy.requireReadable; no SQL writes. | Rows' row-validity boundary. Preserve when the reader policy is consulted; do not precompute it at construction. |
| transactionRow M:3409 and storedTransaction M:3434 | B3 or B6, when first needed | Mapping, byte/hash verification and Base64 encoding; no writes. | Rows; do not build an unused framework ahead of the consumer. |
| storedResource M:2520 | B6 commit closure reader | findResource(FOR UPDATE), state/scope/integrity checks and DTO mapping; no object IO or lease. | Move into Rows when B6 needs it. Do not call B5's public readResource and introduce extra leases, verification updates or quarantine. |
| ensureSessionRoot M:3818 | B1 acquire and B2 lifecycle | R lock plus INSERT IGNORE into ma_event_stream. | Explicitly place this narrow initialization helper in Rows, solely for existing acquire/lifecycle paths. Preserve its call timing, R lock and event-stream INSERT. Do not generalize mutations or make B2 call back into the facade or WriterLifecycle. |
B1 Fencing owns credentials, writer/read grants, placement, CSI continuation/current-time/lease/history-owner checks and lock-error mapping. `checkLifecycleWriter` (M:266), `requireNoLifecycleClaim` (M:482), `requireOrdinaryAdmission` (M:490), and lifecycleFenced/protocolFenced (M:498/510) are shared low-level gates used by writer/resource/commit paths and move into Fencing with their first consumers. B2 LifecycleAuthorization owns the three business authorization entries and the commit policy that authorizes a new lifecycle effect for dispatch; it does not call back into WriterLifecycle. ensureSessionRoot is explicitly assigned to Rows' narrow initialization boundary for its two existing consumers. Do not create Support or move writer state machines there.
The final instance dependency graph is: Rows at the bottom; Fencing→Rows; WriterLifecycle→Rows/Fencing; LifecycleAuthorization→Rows/Fencing plus the existing paired execution/session dependencies; HistoryQueries→Rows/Fencing; ResourceWrites→Rows/Fencing; ResourceReads→Rows/Fencing plus existing retention/object storage; Commit→Rows/Fencing/LifecycleAuthorization/ResourceWrites plus existing extensionRecords/actions/toolResults/transport. Commit's reader callback calls Rows.storedResource directly; writeFence retains its existing call-scoped form. The facade constructs each new object once with final fields. Do not pass the facade or introduce Spring beans. Existing external static lock calls through facade compatibility symbols do not introduce instance callbacks.
commitResources belongs to B4's commit-internal resource writing boundary. Share ValidatedResource through a narrow package-visible value type, including an equivalent nested type if appropriate, without changing its shape. validateCommit remains B6 protocol/full-commit validation; the shared resource value type does not justify moving it early into generic Rows. During B4, the existing facade commit calls ResourceWrites for the complete resource closure; ResourceWrites must not call back into the facade for queries or locks.
@@ -0,0 +1,554 @@
# ManagedAgentStore 与 ManagedSessionStore 拆分设计
[English](2026-11-02-managed-store-decomposition.md) | [简体中文](2026-11-02-managed-store-decomposition.zh-CN.md)
## 状态与范围
本文是已接受 Store 拆分方案的实施契约。基线为 main `e3c7a6d7efc6df6c58ffa8ddd9c42dbb0e1784c4`。R0([#16061](https://github.com/QwenLM/qwen-code/pull/16061))已作为 `1bdad7d4b7949a6f8370c95965ec55901e75f67a` 合入;R1 准备开始时,其 [full gate](https://github.com/QwenLM/qwen-code/actions/runs/41322812090) 仍在运行。合并和验收是两份独立证据。[#16029](https://github.com/QwenLM/qwen-code/pull/16029) 已合入,没有额外前置工作。
R1 交付完整双语契约、下文入口/事务/调用者清单及跨文件锁检查。A1–A9 与 B0–B6 已在 [#16090](https://github.com/QwenLM/qwen-code/pull/16090) 中一并合入,合并提交为 `d9d6fc4a4b8e654d405dbb1d01bf3404e6435a1c`,保留下文逐阶段归属和验收清单。两个 facade 保持稳定,实现仍位于 persistence 内。其 head `66d23ceb1a5bee7e9cb6362cfc238eb64297f3e6` 已通过配置内的 full Managed Agent gate;Store 专项 macOS/Windows 功能资格仍未覆盖。下文精简后续改动单独记录验收证据。
相关设计:[结构评审](2026-10-31-managed-agent-structure-review.zh-CN.md)、[Agent 构造装配](2026-11-01-managed-agent-store-wiring.zh-CN.md)、[Maven 边界](2026-10-29-managed-agent-maven-modules.zh-CN.md)。
## 问题与决策
两个 Store 把事务入口、SQL、可变业务状态和多个不同的操作状态机混在一起。按方法名称搬移会掩盖副作用,也可能使原有单文件锁顺序检查失效。保留稳定 facade 和明确组件归属,使整个提取过程中的事务行为和调用者可以逐项评审。
保留公共业务接口、DTO、异常映射及包可见兼容桥接。新增协作者均为同包、包可见的 `final` 类,由 facade 构造一次。不增加 Spring Bean、Maven 模块、公开业务接口、可变依赖装配或通用 Context/Support 容器。已有 worktree、definition 和 skills 组件继续承担原职责。
组件通过明确类型单向依赖。不为绕开回环新增 facade 回调或构造函数集合。现有 lazy Spring 回环、调用内 fencing 回调和兼容值类型保持原语义。提取需要共享规则时,随首个实际消费者迁移完整依赖闭包,不提前建设未使用的 helper 框架。
## Agent 职责归属
| 切片 | 组件 | 职责 |
| ---- | -------------------------- | ------------------------------------------------------------------------------------- |
| A1 | `AgentSessionQueries` | 普通 Session/Turn 读取、分页、批量摘要、worktree 描述及生命周期展示视图 |
| A2 | `AgentEventJournal` | 公共事件写入、序号与 identity、去重、事件读取及提交通知 |
| A3 | `AgentMessageProjection` | 物化、消息分页、覆盖进度、replay floor 及撤回后的修复 |
| A4 | `AgentSessionPolicies` | model、approval、MCP、extension 的准入与配置提交约束 |
| A5 | `AgentSessionCommands` | Turn 输入/取消命令、rename、一般 Session mutation、命令回执与结算 |
| A6 | `AgentSessionAdmissions` | 普通/workspace/child/内部/CSI Session 准入和重放;definition、skills 与 worktree 准备 |
| A7 | `AgentLifecycleOperations` | close/archive/delete/unarchive、投递扫描及共用 operation claim/retry/completion |
| A8 | `AgentExecutionOperations` | cwd、context compression、task cancel 和接受结果未知的结算,含 compression 投影补写 |
| A9 | `AgentTurnExecution` | dispatch、租约、Harness admission/events、恢复等待、重试及 Turn 终态 |
`AgentStoreRows` 持有共享 Session/Turn 行映射与加锁读取。Commands 持有共享回执/结算,EventJournal 持有事件 identity 和通知,MessageProjection 持有投影修复。高层准入、生命周期和执行工作流依赖这些明确的下层协作者。操作扫描与对应状态机一起迁移。
Queries 不能成为每个 writer 都依赖的另一个 facade。writer 通过 Rows 共享行访问。`contextCompressionState(s)` 会补写投影,归 ExecutionOperations。`deliverableRenames` 会更新投递状态,归 Commands。保留 `markRenameSubmitted` 等写入口原本没有事务注解的行为。
worktree 展示入口 `findSessionWorktree(s)` 和 `listSessions` 会通过已有 `SessionWorktreeStore` 校验及其 `REQUIRES_NEW` 隔离事务持久化 BLOCKED。这三个名称不进入 A1 的纯查询首片;完整实现保留到 A6 将 worktree 协作迁入既定 Queries 展示路径。明确该路径具有副作用,复用现有 worktree 组件,不新增同用途包装或事务。A1 的边界小于最终 Queries 职责。
## Journal 职责归属
| 切片 | 组件 | 职责 |
| ---- | ------------------------------- | -------------------------------------------------------------------------- |
| B0 | 现有 facade 构造装配 | 将八个注入 setter 涉及的九个引用改为构造确定 |
| B1 | `JournalWriterLifecycle` | acquire、renew、seal、quiesce、release、block recovery 及 live-writer 检查 |
| B2 | `JournalLifecycleAuthorization` | ordinary/lifecycle 授权、approval selection 及生命周期派发授权 |
| B3 | `JournalHistoryQueries` | restore、分页/近期事务、command transaction 查询及初始 journal 完整性 |
| B4 | `JournalResourceWrites` | instructions、skills、继承资源、tool result 发布及 commit 内资源写入 |
| B5 | `JournalResourceReads` | 普通/snapshot/owner 读取、验证更新、隔离、读取租约与回执 |
| B6 | `JournalCommit` | 普通/lifecycle commit、CSI terminal 预检/提交、关联投影与通知 |
`JournalStoreRows` 持有共享行访问/映射,以及解释这些行所需的完整性检查。B1 已需要 activation/head 检查和资源回执校验:`activationLeaseRow`、`restoreHead`、`verifyStoredResource` 及其 `toolResultLimit` 依赖随首个消费者迁移。这些校验可以进行只读 capture/quarantine 查询,但不读取对象流、不获取读取租约、不隔离对象,也不更新验证时间戳;这些副作用留在 ResourceReads。其他 helper 仅在实际使用时迁移。
`JournalFencing` 持有凭据、writer 和 CSI fencing,以及有界锁等待错误映射。检查保留在原有的小粒度调用点。WriterLifecycle、LifecycleAuthorization 和资源操作使用 Rows/Fencing;Commit 依赖实际调用的明确资源/授权组件。不让 WriterLifecycle 经 facade 回调 HistoryQueries 或 ResourceReads 来倒置依赖图。
现有 `ensureSessionRoot` 同时被 writer 获取和生命周期授权使用。B1 首次需要时,将此明确且窄的 root 初始化放入 Rows,保留 root 锁及 event-stream INSERT。因此 Rows 是行访问边界,不是只读服务;不把 writer 或 operation 状态机放进去。
## 事务、锁与原子性
每个 facade 入口保留原 transaction manager、传播方式、隔离级别、readOnly 及无注解行为,包括 `MANDATORY`、`NOT_SUPPORTED` 和 `READ_COMMITTED`。普通协作者不创建事务代理,不引入 `REQUIRES_NEW`。原程序式事务的位置和超时保持不变。
同时锁 Session 和 Turn 的路径继续 Session 在前。placement、retention、root、head 保留各路径的现有顺序。CSI continuation 例外和有界锁等待/死锁映射单独保留,不引入新的统一全局顺序。
资源变更、extension/action/message 投影、journal transaction 和 head 更新原子提交。保留重复提交返回位置、writer/CSI 复核、配置读取时点和通知时点。回滚和重放不新增通知,无同步上下文的 standalone 行为不变。
资源读取不是纯查询。普通和 snapshot 读取保留各自的验证/隔离行为及 retention 租约。owner read 继续拒绝环境事务,保持短事务准入、对象读取、短事务复核/回执/收尾。compression 投影补写保留程序式事务。
保留已有外部 `REQUIRES_NEW` 边界:worktree 隔离及 snapshot 调用者的独立 `READ_COMMITTED` 模板。禁止的是新增事务边界,不是删除调用者已经依赖的边界。
提取过程中不改变 SQL、摘要、JSON 或协议字节、schema、HTTP/OpenAPI 契约及算法。
## 构造装配与兼容
B0 在构造时解析必需依赖,并保留 standalone 默认值。可选 lifecycle 双依赖任一缺失时仍两者均不启用。CSI bindings 仍只接受同一数据源上的 JDBC repository。两个维护入口、手工夹具和工厂创建的 Spring Bean 一起迁移。验证必需/可选依赖缺失、真实 lazy 回环、代理身份、实际 `@Bean` 工厂及 standalone 构造。
publication/snapshot 方法、`PublicationWriter`、`TerminalCandidate`、同数据源检查和已有静态锁工具继续从原 facade 提供。CSI terminal 凭据检查、预检和提交必须使用调用方授权绑定的同一物理连接。Agent CSI 准入/凭据桥接及静态 binding 映射也保持兼容。精确桥接归属见入口清单。
## 锁检查器实现
保留基于 JDK 语法树的检查及有界解析模型。加载 facade 与同包的 `Agent*.java` 或 `Journal*.java` 协作类族,不依赖手工逐类更新的允许列表。其他 persistence 文件保留已有边界扫描。新增包含 SQL 的文件仍须显式改变精确固定的 sibling 集合。
跨选定文件按 owner、名称和参数类型采集成员。各文件的 import 和常量环境独立;共享常量使用限定名,避免一个文件覆盖另一文件的 SQL 文本。每个选定源文件及嵌套类的解析成员都与编译后的类交叉核对。
只追踪引用为 final、声明类型是已解析的具体 final 类的字段接收者。字段类型按声明它的类解析。支持裸字段及显式 `this.field`;参数/局部变量遮蔽裸字段时不能遮蔽显式字段访问。外部导入类型不能借用同名已解析协作类的成员或 SQL 常量。不可解析、多态、重载歧义、泛型/局部类和延迟执行体只要候选涉及 Session 锁或 Turn 行,继续按失败处理。不通过扩大歧义豁免或删除集合断言让搬移后的代码通过。
保留现有 facade 入口集合、事务检查、sibling 边界集合,以及歧义/延迟执行体断言。入口集合按所属 facade 采集,不能由协作类的同名方法满足;锁顺序、歧义及编译成员覆盖仍检查整个协作类族。回归覆盖多文件传递委托、把 Session 锁移到 Turn 访问之后、完全删除锁(由固定的 locker 集合捕获)、隐藏到不可解析接收者、遮蔽、歧义重载及文件独立的常量/import。
检查器证明源码层面的顺序,不证明运行时事务语义或数据库无死锁。后续切片仍需相应运行时及 MySQL 证据。
## 实施清单
此表为唯一阶段清单,英文版本完整同步。入口清单是其方法级明细,不另立独立状态表。两种语言同步记录各阶段实现、精确验证版本、相关证据和未完成项。各阶段一起交付;代码已迁移不代表已验收。
| 顺序 | 交付 | 入口清单 | 状态 / 证据 |
| -------- | ----------------------------------------------- | ---------------- | --------------------------------------------------------------------- |
| R0 | 完成 Agent 构造装配及实际 Spring 工厂回归 | 已有装配设计 | #16061 已合入;当前装配由 #16090 full gate 覆盖;保留历史 R0 失败 |
| R1 | 双语契约/清单及跨文件锁检查 | 下文两个清单 | #16090 已合入;full gate 中 59 个锁审计用例及 reactor Checkstyle 通过 |
| A1 | Agent 普通查询及首次需要的行 helper | Agent A1 | #16090 已合入;基线 full gate 通过 |
| A2 | 事件 journal | Agent A2 | #16090 已合入;基线 full gate 通过 |
| A3 | 消息投影 | Agent A3 | #16090 已合入;基线 full gate 通过 |
| A4 | Session 策略 | Agent A4 | #16090 已合入;基线 full gate 通过 |
| A5 | 命令、回执与结算 | Agent A5 | #16090 已合入;基线 full gate 通过 |
| A6 | Session 准入 | Agent A6 | #16090 已合入;基线 full gate 通过 |
| A7 | 生命周期操作 | Agent A7 | #16090 已合入;基线 full gate 通过 |
| A8 | 执行操作 | Agent A8 | #16090 已合入;基线 full gate 通过 |
| A9 | Turn 执行与 Agent facade 收尾 | Agent A9 | #16090 已合入;基线 full gate 通过 |
| B0 | Session 构造装配 | Journal B0 | #16090 已合入;基线 full gate 通过 |
| B1 | 行访问、fencing 与 writer 生命周期 | Journal B1 | #16090 已合入;基线 full gate 通过 |
| B2 | 生命周期授权 | Journal B2 | #16090 已合入;基线 full gate 通过 |
| B3 | 历史查询 | Journal B3 | #16090 已合入;基线 full gate 通过 |
| B4 | 资源写入 | Journal B4 | #16090 已合入;基线 full gate 通过 |
| B5 | 资源读取 | Journal B5 | #16090 已合入;基线 full gate 通过 |
| B6 | 普通/CSI terminal commit 与 Journal facade 收尾 | Journal B6 | #16090 已合入;基线 full gate 通过 |
| 后续精简 | 已评审的精简及架构检查 | 两个 facade / A3 | 已实现;232 个定向用例及 Checkstyle 通过;完整 CI 待验收 |
## 验证与交付标准
R1 不改变 CLI/HTTP 行为,全局 CLI baseline 或模型调用无法验证此源码检查器。使用多文件合成夹具和真实 persistence 源码树,编译 verification 模块,运行 `SessionRowLockOrderTest` 及 reactor Checkstyle。检查双语结构、链接、格式和完整 diff。遵守模块共享本地 Java 预算,保留失败记录,只复用输入未变的证据;新 SHA 或继续会话不会重置预算。此前 272 个装配用例不能验收改动后的检查器。
历史本地证据保存在 `.qwen/investigations/managed-store-split-implementation/`:R1 的 56 个定向锁审计用例及八个 reactor Checkstyle 通过;A1–A9/B0–B6 修复两项装配夹具失败后,17 个已完成测试类中的 260 个不同用例通过。后期两次受限 Maven 调用均超时,原共享账本保持 598.655/600 秒。这些本地命令不改判为成功;当时待补的检查由下文后续基线 CI 证据覆盖。历史 R0 失败仍归档保留,不单独重新归因。
[#16090 的 full Managed Agent gate](https://github.com/QwenLM/qwen-code/actions/runs/41426848917) 已通过,head 为 `66d23ceb1a5bee7e9cb6362cfc238eb64297f3e6`。实际测试 merge 为 `61979c75f943e7383748c75beed02cf1dde977e5`,全部 86 个变更 blob 与 head 一致。unit/H2:6,098 个用例,跳过 17 个;MySQL 8.4.6:1,242 个用例,无跳过;Hosted:167 个用例,跳过四个;fault:45 个用例,无跳过;Hosted process gates:两个用例,无跳过;O4:59 个用例,无跳过。已完成测试的失败和错误均为零。59 个锁审计用例、六个生产模块 SpotBugs、八个 reactor Checkstyle、Hosted failover 及两个 EventTransport profile 均通过。跳过项不算通过。macOS/Windows 车道编译 reactor 并运行 SDK/Runtime Broker 测试,没有运行 Store 功能套件,该平台缺口继续保留。这些证据验收已合入基线的配置内检查,不验收后续精简改动、真实集群或部署。
精简后续改动从 main `73474ef094ea1c6bb2a95a9a2a33a28b0ec58e9e` 开始,删除 18 个协作类重复默认重载及三个无调用内部方法。facade 入口直接向全参数协作类方法传入原默认值,保留签名、事务属性以及经协作类分派的行为,不转调另一个 facade 重载。消息行直接映射为 `ItemRecord`,通过 `List.copyOf` 保持原分页列表不可变性。可选的 Rows 转发精简继续后置。架构检查通过反例覆盖拒绝 `@Service`/`@Repository` 等 Spring 元注解和任意大小写的 SQL 关键字;分页断言保护列表不可变性。锁审计仅移除已删除的 `beginLifecycle/9` 成员与 `ItemRow.toRecord/0` 豁免,保留 facade 集合、编译成员覆盖和失败关闭反例。
精简改动本地验证的 14 个选定测试类共 232 个用例全部通过,失败、错误、跳过均为零,包含全部 59 个锁审计用例及五个拆分检查用例。八个 reactor Checkstyle 均通过。静态参数替换核对确认 18 条默认路径到达相同的全参数实现,其他 633 个已解析成员正文未变;这些静态检查不是运行时证据。首次末尾空行 Checkstyle 失败已修复,并保留在本次调查记录中。后续源码编辑仅调整注释及调用换行,由最终 Checkstyle 和默认路径核对覆盖。新版本的完整 verify、SpotBugs、真实 MySQL、Hosted/fault 和 full gate 仍待 CI;Store 专项 macOS/Windows 功能资格仍未覆盖。本次精简是 #16090 合入后用户单独选择的新改动,使用自己的共享 600 秒本地预算;原 PR 账本和证据保持原状,不作为新输入的验收证明。
各已实现阶段的验收需覆盖 tenant/actor 隔离、游标语义和批量查询次数;含 definition/skills/worktree 的准入/重放;序号/去重/投影缺口/撤回/replay floor;提交后通知及回滚/重放不通知;租约 generation/token/到期及接受结果未知时不重复派发;journal 原子回滚/重放;资源损坏/读取租约失效/owner read 收尾;publication/snapshot 锁顺序和 CSI 同连接原子性。
SQL/并发路径需要真实 MySQL 证据。CI 承担完整 verify、SpotBugs、完整 MySQL、Hosted/故障及跨平台检查。当前 push 的 fast tier 缺少必需资格项,接受版本的 full Managed Agent gate 才是交付标准。所有未运行项都单独列明。
每片均需完整 diff 连续两轮干净自审及独立评审。最终两个 facade 只保留装配、事务入口和兼容转发,不包含业务 SQL 或状态机。协作者实例依赖无环,公共契约和各路径事务语义保持一致。定向测试通过不等于完整验收。
## 风险与待决事项
已接受的架构方向没有未决产品问题。主要实施风险是隐蔽查询副作用、共享回执/资源校验、lazy 构造回环、事务边界搬移和源码检查盲区。发现意外依赖时,在最小受影响切片内解决,并在改变设计前同步两个语言版本。不把算法、SQL、存储或协议重设计悄然并入搬移 PR。
## 已实现的依赖边界
facade 按原顺序只计算一次配置和 catalog 校验,再把相同值和依赖实例传给协作类的 final 字段。Agent 构造顺序为 Rows → Queries/EventJournal → Policies/Projection → Commands → Admissions/Lifecycle/TurnExecution → ExecutionOperations。Journal 构造顺序为 Rows → Fencing → WriterLifecycle/HistoryQueries/LifecycleAuthorization/ResourceWrites/ResourceReads → Commit。协作类不接收 facade 或构造器回调集合。
共享 Session/Turn 单点读取位于 Rows;Queries 的三个兼容读取入口转发至这些实现。Policies 持有共享准入谓词,Rows 持有它们已有的 key 编码和数据库时间读取。九参数 Session 准入默认方法仍在 facade 上按原样转发到接口默认实现,保留虚分派和事务边界。资源行校验与对象 IO、租约、隔离分别保留。Commit 在原位置调用提取出的生命周期派发授权块,传入已有的调用内资源 reader。
Session 构造现在确定原 setter 涉及的全部九个引用。Spring 必需依赖仍必需;standalone 构造保留原 null/in-process/unbound 默认值。生命周期双依赖任一缺失时,两者均不启用。CSI 仅接受同数据源 JDBC runtime binding repository。维护入口与夹具在创建下游消费者前传入依赖;实际 Spring 工厂构造另有覆盖。
锁检查保留 facade 入口集合,并按 owner/name/arity 钉住删除未使用的 `beginLifecycle/9` 后剩余的 23 个已迁移 private helper 重载。协作类与完整源文件族一起分析,同时逐文件保留显式 SQL 归属边界集合。编译成员覆盖、不可解析接收者的拒绝规则和原有反例仍为必需。下文原行号清单指记录的基线,不指迁移后位置。
## Agent 入口清单
源码:`packages/managed-agent-server/persistence/src/main/java/com/alibaba/qwen/code/managedagent/persistence/ManagedAgentStore.java`。
事务图例:**T** = 保留原始普通 `@Transactional`(默认 REQUIRED、DEFAULT 隔离级别、readOnly false),facade 类保留 `@Qualifier("managedAgentTransactionManager")`;**M** = 在上述基础上显式使用 MANDATORY;**RR** = 显式 transactionManager `managedAgentTransactionManager`、readOnly true、REPEATABLE_READ;**N** = 当前入口没有声明式事务。N 不代表只读,也不授权新增事务。worktree 行另行标明其现有的验证/隔离事务。
所有重载均按源码行号列出。两个构造器保留为 facade 装配;public static `readBinding` 继续在 facade 提供,可转发到行组件。下列生产调用者已按实际接收者声明核对,并单独检查了 CSI 本地 `var` 构造路径;未发现调用的入口明确注明,不意味着可以删除该方法。即使生产代码当前未调用某个兼容入口,测试仍是其消费者。
### A1 AgentSessionQueries
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| ------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `findChildWorkspaceCwd` (691) | N | ManagedAgentService |
| `findChildLineage` (980) | N | ManagedAgentService, ManagedUserMessagePolicy, QwenHostedHarnessConnector |
| `hasCompletedWorkspaceClose` (2283) | N | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `completedWorkspaceCloses` (2290) | N | ManagedAgentService |
| `hasExecutionRegistryFacts` (3911) | N | ManagedActionStore, ManagedUserMessagePolicy |
| `sessionsWithExecutionRegistryFacts` (3919) | N | ManagedAgentService |
| `findSession` (4544) | N | ManagedAutomationService, WorkspaceCsiRuntimeAccess |
| `findSessionById` (4556) | N | ManagedRuntimeCoordinator, WorkspaceRuntimeResolver |
| `findLatestLifecycleOperations` (4648) | N | ManagedAgentService |
| `findTurn` (4674) | N | HarnessCoordinator, ManagedAgentService, QwenHostedHarnessConnector, SessionRecoveryService |
| `findActiveTurns` (4684) | N | ManagedAgentService, QwenHostedHarnessConnector |
| `findLatestTurns` (4708) | N | ManagedAgentService, SessionRecoveryService |
| `findModelContexts` (4747) | N | ManagedAgentService |
| `modelOptions` (4753) | N | ManagedAgentService |
| `findModelSelections` (4756) | N | ManagedAgentService |
| `findTurnModelSelection` (4776) | N | QwenHostedHarnessConnector |
| `findApprovalSelections` (4862) | N | ManagedAgentService, QwenHostedHarnessConnector |
| `findApprovalOwners` (4881) | N | ManagedAgentService |
| `findTurnApprovalSelection` (4895) | N | QwenHostedHarnessConnector |
| `listTurns` (5086) | N | ManagedAgentService |
| `findTurnSummary` (5110) | N | ManagedAgentService |
| `findLastVisibleStatusBeforeDelete` (6634) | N | ManagedAgentService |
| `requireSession` (6647) | N | ChildWorkspaceService, HarnessCoordinator, ManagedAgentService, ManagedExtensionCommandVerifier, ManagedExtensionRecordStore, ManagedRuntimeCoordinator, ManagedSessionStore, ManagedUserMessagePolicy, QwenHostedHarnessConnector, SessionLifecycleCoordinator, SessionLifecycleService, SessionRenameCoordinator |
### A2 AgentEventJournal
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| --------------------------------------- | ---- | -------------------------------------------------------------- |
| `findEvents` (5122) | N | ManagedAgentService |
| `findLatestEnvironmentEvents` (5140) | N | ManagedAgentService |
| `findControlEvents` (5184) | N | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `findTranscriptEvents` (5199) | N | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `appendPublicEventIfAbsent` (6601) | T | HarnessCoordinator, ManagedActionStore, ManagedToolResultStore |
| `appendLiveSessionEventIfAbsent` (6612) | T | 未发现外部直接生产调用;保留接口、内部及测试用法 |
### A3 AgentMessageProjection
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| ------------------------------------ | ---- | ------------------- |
| `findMessagePage` (5234, 5242) | RR | ManagedAgentService |
| `findMessageCoveredSequences` (5413) | N | ManagedAgentService |
| `findReplayWindow` (5435) | N | ManagedAgentService |
| `findReplayFloorTargets` (5454) | N | ReplayFloorAdvancer |
| `advanceReplayFloor` (5475) | T | ReplayFloorAdvancer |
| `findMaterializationTargets` (5506) | N | MessageMaterializer |
| `materializeNextBatch` (5516) | T | MessageMaterializer |
| `deferMaterializationTarget` (5620) | T | MessageMaterializer |
### A4 AgentSessionPolicies
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| ------------------------------------------------ | ---- | --------------------------------- |
| `workspaceSessionOptions` (1096) | T | ManagedAgentService |
| `supportsPlanMode` (1113) | N | ManagedAgentService |
| `hasMcpServers` (1169) | N | SessionLifecycleCoordinator |
| `requireMcpManagement` (1180) | T | ManagedMcpManagementService |
| `requireMcpSettingsCommit` (1215) | M | ManagedExtensionRecordStore |
| `requireExtensionManagement` (1228) | T | ManagedExtensionManagementService |
| `requireConfigurationCommit` (1266) | M | ManagedExtensionRecordStore |
| `workspaceFilesEnabled` (2249) | N | ManagedAgentService |
| `workspaceShellEnabled` (2254) | N | ManagedAgentService |
| `changeSessionModel` (4790) | T | ManagedAgentService |
| `changeSessionApproval` (4926, 4935, 4944, 4954) | T | ManagedAgentService |
### A5 AgentSessionCommands
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| ------------------------------------ | ---- | ------------------------------------------------ |
| `insertTurnCommand` (1514, 1525) | T | ManagedAgentService |
| `insertCancelCommand` (1620, 1629) | T | ManagedAgentService |
| `beginSessionRename` (1685, 1693) | T | ManagedAgentService |
| `completeSessionRename` (1833, 1840) | T | ManagedAgentService, SessionRenameCoordinator |
| `abandonSessionRename` (1918) | T | ManagedAgentService, SessionRenameCoordinator |
| `deliverableRenames` (1933) | T | SessionRenameCoordinator |
| `claimRename` (1953) | T | ManagedAgentService, SessionRenameCoordinator |
| `markRenameSubmitted` (1979) | N | ManagedAgentService, SessionRenameCoordinator |
| `retryRename` (1997) | T | ManagedAgentService, SessionRenameCoordinator |
| `beginSessionMutation` (2080, 2089) | T | ManagedAgentService |
| `completeSessionMutation` (2151) | T | ManagedAgentService |
| `abandonSessionMutation` (2225) | T | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `replayCommand` (4502) | N | ManagedAgentService |
| `findCommand` (4524) | N | ManagedAgentService |
### A6 AgentSessionAdmissions
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| -------------------------------------------- | ------------------------- | ------------------------------------------------------ |
| `insertSessionCommand` (388, 7651) | T | ManagedAgentService |
| `insertWorkspaceSessionCommand` (402) | T | ManagedAgentService |
| `insertInternalTeamLeadSessionCommand` (528) | T | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `findWorkspaceCreateReplay` (555) | N | ManagedAgentService |
| `replayWorkspaceSessionCommand` (570) | T | ManagedAgentService |
| `insertChildSessionCommand` (667, 677) | T | ManagedAgentService |
| `insertPublicChildSessionCommand` (703) | T | ManagedAgentService |
| `replayChildSessionCommand` (952) | T | ManagedAgentService |
| `insertCsiSessionCommand` (582) | N(要求环境事务) | WorkspaceCsiSessionCommands |
| `requireCsiRequest` (633) | N(加锁桥接;调用方事务) | WorkspaceCsiRuntimeAccess, WorkspaceCsiSessionCommands |
### A6 -> AgentSessionQueries:兼容展示路径
这些入口在 A1-A5 期间保持 facade 中的原实现。A6 处理 worktree 协作时,将它们迁入既定的 AgentSessionQueries 组件。副作用继续委托现有 SessionWorktreeStore,不引入额外的 worktree 包装层。
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| --------------------------------- | ------------------------------- | --------------------------------------- |
| `findSessionWorktree` (157) | N + 现有 worktree 验证/隔离事务 | HarnessCoordinator, ManagedAgentService |
| `findSessionWorktrees` (162) | N + 现有 worktree 验证/隔离事务 | ManagedAgentService |
| `listSessions` (4564, 4569, 4574) | N + 现有 worktree 验证/隔离事务 | ManagedAgentService |
### A7 AgentLifecycleOperations
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| -------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------- |
| `beginOperation` (2238) | T | SessionLifecycleService |
| `beginWorkspaceClose` (2260) | T | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `beginWorkspaceLifecycle` (2268, 2277) | T | SessionLifecycleService |
| `unarchiveWorkspaceSession` (2322) | T | ManagedAgentService |
| `beginChildClose` (2643) | T | ChildLifecycleAdmissions |
| `findOperation` (4062) | N | ManagedActionStore, ManagedAgentService, SessionLifecycleService, TaskCancelCoordinator, TurnInputEvidence |
| `findDeliverableOperations` (4070) | N | SessionLifecycleCoordinator |
| `claimOperation` (4108) | T | ActionResponseCoordinator, SessionLifecycleCoordinator, TaskCancelCoordinator |
| `completeOperation` (4143) | T | SessionLifecycleCoordinator |
| `failOperation` (4243) | T | SessionLifecycleCoordinator |
| `renewLifecycleOperation` (4308) | T | ActionResponseCoordinator, SessionLifecycleCoordinator, TaskCancelCoordinator |
| `blockLifecycleOperation` (4363, 4371) | T | SessionLifecycleCoordinator |
| `retryOperation` (4418, 4427) | T | ActionResponseCoordinator, SessionLifecycleCoordinator, TaskCancelCoordinator |
| `markOperationAnswered` (4473) | T | ActionResponseCoordinator |
### A8 AgentExecutionOperations
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| ------------------------------------------- | ---- | -------------------------------------------------------- |
| `beginContextCompression` (2930) | T | ContextCompressionService |
| `contextCompressionState` (3005) | N | 未发现外部直接生产调用;保留接口、内部及测试用法 |
| `contextCompressionStates` (3020) | N | ManagedAgentService |
| `contextCompressionRequest` (3169) | N | ContextCompressionCoordinator, ContextCompressionService |
| `findContextCompressions` (3177) | N | ContextCompressionCoordinator |
| `claimContextCompression` (3186) | T | ContextCompressionCoordinator |
| `requireContextCompressionCommit` (3200) | N | ManagedExtensionRecordStore |
| `markContextCompressionSubmitted` (3217) | T | ContextCompressionCoordinator |
| `settleContextCompression` (3231) | T | ContextCompressionCoordinator |
| `retryContextCompressionPreparation` (3251) | T | ContextCompressionCoordinator |
| `awaitContextCompression` (3263) | T | ContextCompressionCoordinator |
| `completeCwdChangeOperation` (3289) | T | SessionLifecycleCoordinator |
| `failCwdChangeOperation` (3406) | T | SessionLifecycleCoordinator |
| `beginTaskCancelOperation` (3434) | T | ManagedTaskCancelService |
| `findDeliverableTaskCancels` (3539) | N | TaskCancelCoordinator |
| `findParkedOperations` (3564) | N | OperationReconciler |
| `settleParkedOperation` (3582) | T | AcceptanceEvidence, OperationReconciler |
| `turnInputAcceptance` (3641) | N | TurnInputEvidence |
| `renameAcceptance` (3703) | N | RenameEvidence |
| `settleParkedRename` (3733) | T | RenameEvidence |
| `settleTaskCancel` (3804) | T | TaskCancelCoordinator |
### A9 AgentTurnExecution
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| --------------------------------------- | ---- | ---------------------- |
| `findDispatchable` (5627) | N | HarnessCoordinator |
| `findRuntimeRecoveryParked` (5639) | N | HarnessCoordinator |
| `claimTurn` (5654) | T | HarnessCoordinator |
| `renewTurn` (5675) | T | HarnessCoordinator |
| `releaseTurnLease` (5685) | T | HarnessCoordinator |
| `markTurnRecoveryBlocked` (5698) | T | HarnessCoordinator |
| `markTurnFailureRecovery` (5721) | T | HarnessCoordinator |
| `requestRecoveryRecheck` (5739) | T | HarnessCoordinator |
| `parkTurnForRuntimeRecovery` (5759) | T | HarnessCoordinator |
| `deferTurnRetry` (5779) | T | HarnessCoordinator |
| `scheduleTurnRetry` (5786) | T | HarnessCoordinator |
| `bindHarness` (5804) | T | HarnessCoordinator |
| `bindRecoveredHarness` (5831) | T | HarnessCoordinator |
| `markSubmissionAttempted` (5862) | T | HarnessCoordinator |
| `withdrawSubmissionAttempted` (5887) | T | HarnessCoordinator |
| `recordAdmission` (5904) | T | HarnessCoordinator |
| `recordRecoveryAdmission` (5948) | T | HarnessCoordinator |
| `retractContinuationOutput` (6009) | T | HarnessCoordinator |
| `retractHarnessTurnOutput` (6079, 6087) | T | HarnessCoordinator |
| `recordHarnessEvents` (6304) | T | HarnessCoordinator |
| `cancelUnadmittedTurn` (6461) | T | HarnessCoordinator |
| `cancelBeforeAdmission` (6505) | T | HarnessCoordinator |
| `failTurn` (6535) | T | HarnessCoordinator |
| `failWorktreeInitialTurns` (6541) | T | SessionWorktreeService |
| `failRecoveryBlockedTurn` (6562) | T | HarnessCoordinator |
### Rows 兼容转发
| 入口 / 重载行号 | 事务 | 直接生产调用者 |
| -------------------- | ---- | ------------------------------------------------------------------- |
| `readBinding` (7569) | N | WorkspaceFileStore, WorkspaceMigrationMain, WorkspaceMigrationStore |
清单覆盖 **136 个不同入口名 / 153 个声明**,包括两个包可见的 CSI 桥接方法和一个 public static 映射器,不包括两个构造器。
### worktree 展示路径的决定与副作用
worktree 展示入口不是纯读取。`findSessionWorktree` / `findSessionWorktrees` 调用 `SessionWorktreeStore.descriptor(s)`(107-145),`listSessions` 在批量分页 SQL 后调用 `descriptors`(4645)。`verifiedDescriptor`(112-120)验证文件系统身份并读取当前分支;遇到 `ManagedAgentException` 时调用 `blockIdentity`(368-378),后者通过已有的 `TransactionTemplate` 和 `PROPAGATION_REQUIRES_NEW` 将 READY 改为 BLOCKED 并增加 revision。该独立事务特意保证调用方准入失败回滚后,隔离结果仍然保留。最终决定:首个 A1 纯查询切片排除这三个入口名,保留其 facade 原实现直到 A6。A6 将它们迁入既定 AgentSessionQueries 组件,作为明确带副作用的兼容展示路径,复用 SessionWorktreeStore 验证/隔离及其现有事务。不得添加 readOnly、跳过验证、复制隔离代码、删除现有事务,也不增加另一层 worktree 包装。
策略/执行准入也会通过 `requireCwdChangeRegistryFacts`(3894)进入同一条 worktree 验证路径:先执行 `SessionWorktreeStore.requireReady`,再执行 `hasExecutionRegistryFacts`。必须保留 worktree 验证先于 creator/binding 事实检查的顺序,包括持久化隔离语义。
### 事务与桥接例外
- `findWorkspaceCreateReplay`(555)没有事务声明;`replayWorkspaceSessionCommand`(570)带普通 `@Transactional`。二者都会重新检查重放摘要、internal-team-lead 身份、workspace 绑定和当前读取权限。不得统一这两种事务模式。
- `insertCsiSessionCommand`(582)为包可见方法,没有事务注解,显式要求活动事务并拒绝 journal event transport。`requireCsiRequest`(633)保留相同的 transport 隔离,锁定 Session 和 runtime-request key,并验证 CSI profile/request 身份。调用方 `WorkspaceCsiSessionCommands.create`(19-47)拒绝环境事务,创建新的 10 秒 TransactionTemplate,检查原始物理事务连接,先锁 placement 再锁 retention,验证 registration,随后在该连接上调用两个 Store 桥接入口。此调用方使用 standalone 的 `var sessions = new ManagedAgentStore(...)` 构造路径。
- `requireCsiRequest` 也由 `WorkspaceCsiRuntimeAccess.request`(183-190)调用;该调用方原有的 continuation/retention 锁行为全部保留。保留 facade 方法的包可见性和七参数 standalone 构造器。
- `readBinding`(7569)是 public static 方法,由 `WorkspaceMigrationMain`、`WorkspaceMigrationStore` 和 `WorkspaceFileStore` 直接使用;实际映射迁入行组件后,仍需保留 facade 转发入口。
- `requireMcpSettingsCommit`(1215)和 `requireConfigurationCommit`(1266)使用 MANDATORY。`requireContextCompressionCommit`(3200)没有注解;它在 `ManagedExtensionRecordStore` 的已有提交流程中被调用,不得额外增加 MANDATORY/REQUIRES_NEW 包装。
- `findMessagePage` 重载(5234/5242)显式使用 managedAgentTransactionManager、readOnly true 和 REPEATABLE_READ。普通 Query 入口没有注解,应继续保持。
- `deliverableRenames`(1933)是事务内的修改型扫描:先停放过期的 SUBMITTED claim、重置过期的 PREPARING claim,再选择待处理工作。`markRenameSubmitted`(1979)是没有声明式事务的带条件写入。二者均归 Commands,并保留该差异。
- `contextCompressionState`(3005)调用 `ensureCompactionProjection`(3066);缺少投影时,通过 DataSourceTransactionManager 创建默认 TransactionTemplate 并重放 journal 行。批量方法 `contextCompressionStates`(3020)可能回落到此写路径,并标记不可用状态。二者归 A8,保留同一程序式事务,不归 A1。
- `publishAfterCommit`(7489)仅在事务同步提交后发布;没有同步上下文时立即发布。空事件不执行任何操作。EventJournal 提取必须保留 standalone 回退行为,不得增加重放或回滚通知。
### 实际锁路径(不统一为一个全局顺序)
- 行 helper `requireSessionForUpdate`(7061/7066)先锁 PUBLIC Session 行,仅在取得该锁后才读取并锁定对应 event-stream 计数器。列表映射器与严格单 Session 映射器对缺少 stream 的处理不同。需要同时锁二者的路径随后通过 `requireTurnForUpdate`(7106)锁 Turn;提取后保留其 NOT_FOUND 映射。
- 许多准入、修改和策略入口首先执行 `WorkspaceMigrationAdmission.lockTenant`,然后按路径依次处理 Session/stream、Turn/receipt/operation。MCP/extension 的 idle 复核(1340)专门按此顺序进行新鲜加锁读取:Session plan -> 活动 Turn -> 未结算 child-run record -> requested action records -> 活动 operation/rename/receipt。已有 journal 快照是这些读取不能改成普通一致性读取的原因。
- 生命周期准入 `beginLifecycle`(2695)先锁 migration tenant,按条件锁 retention tenant,再锁 Session/stream;保留失败操作的重放/恢复、状态授权和 idle journal 检查,按原顺序写 operation/fence/head 及事件。`completeOperation`(4143)先执行 legacy CSI guard,再按条件锁 placement 和 bounded retention,随后根据生命周期协议在 Session 之前或之后加 deletion 锁,最后处理 operation claim。不得统一其分支顺序。
- `ManagedLegacySessionGuard.requireLegacyMutation` 本身通过 `ManagedSessionStore.namedRootDeadlock` 进行使用索引的 CSI root 加锁读取,查询超时为 10 秒。一些 Turn lease 路径采用此 guard 加带条件 UPDATE,准入/终态路径则取得完整 Session/stream 锁后再处理 Turn。保留差异;通用的行 helper 事务或锁会改变语义。
- 物化和 replay-floor 推进在检查 Turn 证据或更新 coverage 之前先锁 Session/stream。撤回修复 `rebuildTurnText`(6172)只在外层 Turn 工作流已经取得原有 owner/epoch 锁之后,才锁 stream coverage 和 message 行。
### A1 依赖闭包与边界
A1 普通查询子集需要 JdbcTemplate、已配置的 ObjectMapper、HarnessRules(用于审批规则解码)、构造时固定的 modelCatalog 快照及 Session/Turn 映射器。WorktreeFiles 和三个 worktree 展示入口推迟到 A6 -> AgentSessionQueries 交付。共享行职责包括 `readSession`、sessionMapper/sessionListMapper、turnMapper/turnSummaryMapper、readBinding、nullable/additive 列 helper、StreamCounters 和已有的加锁 Session/Turn 读取器;各项只在首次实际需要时迁移,保留严格读取与列表读取的 stream 差异及全部 schema projection。JSON 解码仅在具体行映射器确实需要时共享,不引入通用 Support/Context 对象。
`findLatestTurns` 按最后一个 turn.accepted 事件选择,而非最新 gmt_create。A6 迁移 `listSessions` 时,必须保留 tenant/actor 授权、archive/title/cursor 过滤、limit+1 语义、批量 worktree 元数据及验证/隔离;查询次数、顺序和字节表示不变。`findSessionById` 是运行时解析已有的跨 tenant 查询,不得擅自增加 tenant 过滤,也不得将其暴露为新的 HTTP 操作。
单点读取和展示视图可以归 Queries;操作任务扫描跟随其状态机(rename -> Commands、materialization/replay floor -> Projection、compression/task cancel/parked -> ExecutionOperations、lifecycle dispatch -> LifecycleOperations、Turn dispatch/recovery -> TurnExecution)。策略、命令和生命周期共享的只读 Session busy 谓词,应在首次迁移时确定唯一的窄职责持有者,不得为了复用它们而让组件回调 facade。
### 后续切片的具体单向协作边界
- A2 EventJournal 持有 appendEvent/appendEvents、event identity/source-key 处理、eventMapper 和提交后发布;可以依赖 Rows,不依赖工作流组件。它不执行同步消息物化。
- A3 MessageProjection 持有投影扫描、物化、replay-floor 状态及 `rebuildTurnText` 修复 helper。事件读取依赖单向指向 EventJournal/Rows。public `retractContinuationOutput`(6009)和两个 `retractHarnessTurnOutput` 重载(6079/6087)仍是 A9 工作流入口:owner/lease/epoch 检查以及 event/identity 删除先于 A3 修复,并处于同一 facade 事务。不得给 Projection 添加指向 TurnExecution 或 facade 的回调。
- A4 策略事务拥有现有的 actor 作用域 SET_MODEL/SET_APPROVAL 重放协议,直接使用 ManagedReceipts(4790 起、4954 起)。它们不调用通用 insertCommand/settleTurnCommand。保留这些作用域和原有 result JSON,不将其重写到新的通用 receipt 流程。A4 可以使用 Queries/Rows/EventJournal 和既有 definitions/skills;后续 Commands/Admissions 单向使用策略检查。
- A5 Commands 持有共同的 `insertCommand`(7156/7163/7170)、`settleTurnCommands`(2477)、`settleTurnCommand`(2532)、Turn command operation 插入(2416)、命令重放和回执。A9 的 recordAdmission/recordRecoveryAdmission/recordHarnessEvents 和终态路径调用此结算边界。A8 接受结果对账必须使用相同的命令/rename 结算语义;Commands 不反向回调 A8/A9。
- A6 Admissions 依赖下层 policy/command/event/row 组件和既有 definitions/skills/worktree 组件。保留先重放、再进行新准入解析的时点,以及 creator/child 作用域、definition 固定和回滚行为。
- A7 LifecycleOperations 持有 operation 单点读取、重放、claim/lease/retry/completion 和 operation 映射器。当前共享 API 由 SessionLifecycleCoordinator、ActionResponseCoordinator 和 TaskCancelCoordinator 使用;facade 路由保持不变。A8 按需调用 A7 的 operation 查询及共同 claim 原语,例如 claimContextCompression(3186)完成专用 CAS 后读取 findOperation;requireContextCompressionCommit(3200)通过同一查询检查操作。协调器已经负责选择通用 completion 或专用 cwd/task-cancel 结算;A7 不得变成调用 A8/A9 的派发器。
- A8 ExecutionOperations 持有专用 cwd/compaction/task-cancel/unknown-acceptance 状态机和对应扫描,向下依赖 Rows/Queries/Policies/Commands/EventJournal 及 A7 共同 operation 原语。通用 claim/retry 不接收用于专用结果处理的回调。
- A9 TurnExecution 需要 Rows/Queries、共同 Commands 结算、EventJournal 和 Projection 修复。不能仅因协调器另行调用生命周期 API 就让它依赖 LifecycleOperations,应保留两者分离,避免形成环。
- 实例构造无环是每次提取的显式验收项,不能仅凭这些职责标签宣称已证明。尤其是 hasOpenOperation/hasOpenExecutionOperation/hasPendingRenameDelivery 等共享谓词和已有 schema key 编码器,不得形成 Policies -> Commands -> Policies 或 Commands -> Lifecycle -> Commands 环。应迁移真实依赖闭包,并在适当位置直接使用现有 schema/receipt helper。无需构造器函数集合、facade 回调或可变 setter。
## Journal 入口清单
基线:`e3c7a6d7efc6df6c58ffa8ddd9c42dbb0e1784c4`(2026-11-02)。下文 `M:<line>` 指 `packages/managed-agent-server/persistence/src/main/java/com/alibaba/qwen/code/managedagent/persistence/ManagedSessionStore.java`,共 3931 行。同包文件简称均指该 persistence 目录;Controller 位于 server,Hosted connector 位于 adapters。本清单只定义迁移边界,不代表后续切片已实现或通过验收。
### 1. 结论与边界
- 实现公开业务方法共 27 个签名、22 个名称,全部来自现有 contracts `repository.ManagedSessionStore`。构造器 3 个,装配 setter 8 个、引用 9 个;另有包内方法 17 个、包内 record 2 个、锁等待常量 1 个。下面全部列出。
- 该 Store 没有 `MANDATORY` 方法。不能把整体方案提到的 `MANDATORY` 错填到 Journal 入口;Journal 的显式特殊事务是两个 `READ_COMMITTED` history 查询和 `NOT_SUPPORTED` 的 initial-journal 检查,外加无声明式事务的资源读取及包内桥接。
- 新增内部对象不要调用 facade。B1 所需的资源行完整性检查、activation 行映射随首次实际使用进入 `JournalStoreRows`,因此 B1 不需要等待 B3/B5 或回调 facade。对象读取、lease、更新验证时间和 quarantine 均不进入 Rows。
- 公开业务接口、既有包内入口、`PublicationWriter`、`TerminalCandidate`、静态锁工具与常量继续从 facade 提供;内部 helper 的包可见化只为显式类型依赖,不能新增通用 Context/Support。
- 锁顺序按路径保留:普通 heartbeat 不获取 tenant guard,CSI continuation 先锁 root 再获取 placement/retention;publication/snapshot 又有自身先行 legacy guard/probe 和调用者锁。不要把这些路径概括成一个强制全局顺序。
### 2. 事务图例
`T`:原方法 `@Transactional`;manager 由类上 `@Qualifier("managedAgentTransactionManager")`(M:82)保持现有解析,注解未指定显式 manager;传播 `REQUIRED`,隔离 `DEFAULT`,`readOnly=false`,默认 timeout/rollback。两个 overload 的注解分别保留,原 self-call 不产生额外代理边界。
`RO`:同一 manager,`REQUIRED` / `DEFAULT` / `readOnly=true`。
`RC`:同一 manager,`REQUIRED` / `READ_COMMITTED` / `readOnly=true`。内部调用 `restore` 目前是 self-call,不能迁移成额外事务。
`NS`:同一 manager,`NOT_SUPPORTED` / `DEFAULT` / `readOnly=true`;挂起调用者事务,自己不创建事务。
`N`:无声明式事务;不得因为提取而新增代理或注解。是否在事务内取决于现有调用者。特别是 standalone 构造器从来不会自动建立 Spring 方法事务。
`P`:`N` 外壳,程序式 owner-read 流程,见第 6 节。
以下锁轨迹记录原有顺序及分支,省略重复取得同一已持有锁,不表示可以删除这些重复调用。`P`(锁列内)= placement tenant guard;`Tg`= output-publication retention tenant guard;`R`= session root;`H`= managed-session head;`D`= harness drain head;`C`= CSI binding/slot/retirement/progress 原连接锁闭包。所有 bounded tenant 等待继续 5 秒,namedRootDeadlock 仅映射其现有死锁集合。
### 3. 公开业务入口完整矩阵
| 原入口和行号 | 归属 | 事务 | 原主要锁/副作用 | 生产调用者 |
| ------------------------------------------- | -------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `authorizeLifecycle` M:211 | B2 JournalLifecycleAuthorization | T | credential → P → Tg → ensure R/event stream → placement → session row → lifecycle claim → H;有 kind 时验证有效 writer,无 kind 的 DRAINING 分支只校验身份/状态 | ManagedSessionStoreController:102 |
| `authorizeOrdinary` M:394 | B2 | T | credential → R → placement → ordinary claim/drain admission(legacy-close 有例外)→ config checks → H → writer;不新增 tenant guard | Controller:63,93 |
| `approvalSelection` M:425 | B2 | T | 调用 ordinary 授权后 `ma_session FOR UPDATE`;返回模型/approval/permission selection,不是纯查询 | Controller:72 |
| `acquireWriter` M:629,635(4/5 参数) | B1 JournalWriterLifecycle | T/T | lifecycle authority 分支先 P→Tg→R→claim;公共核心 P→Tg→CSI/R→physical-open→ensure R/event stream→placement/control storage→materialize D→claim/admission→session row→H;创建/续用/换 generation;cold CSI takeover 经绑定与 history proof | Controller:113;大量手工 journal fixtures |
| `renewWriter` M:807,813(4/5 参数) | B1 | T/T | authority 分支的 guards 保留;placement→CSI continuation(ordinary R,pinned R→P→Tg→C)→H;普通 heartbeat 不加 Tg;writer 和 CSI lease 复核;activation renew 还更新 journal tx/head | Controller:124 |
| `sealWriter` M:891,897(4/5 参数) | B1 | T/T | authority guards→placement→lockCsiOriginal(private CSI 拒绝普通 seal)→R/H;sealed 相同身份提前 replay;writer 允许过期但不允许身份变更 | Controller:135 |
| `quiesceOwnership` M:938 | B1 | T | credential placement identity→placement current→R/H→writer→更新 session QUIESCING | Controller:152 |
| `releaseOwnership` M:952 | B1 | T | P→Tg→R→已有 receipt/resource 的 FOR UPDATE 校验与提前 replay;新请求 placement/claim→H→writer/proof→resource/record/ref→head SEALED→session RELEASED,同一事务 | Controller:144 |
| `blockRecovery` M:1051 | B1 | T | placement→CSI continuation/R→H;writer/CSI current 先于 replay,并在更新 recovery 前再次复核 | Controller:162 |
| `commit` M:1112,1118(4/5 参数) | B6 JournalCommit | T/T | authority guards→P→placement→CSI/R/C/可选H→查 command;duplicate 分支提前返回;新提交 physical-open→R/H→resource/projection/journal/head,同一事务。详见第 7 节 | Controller:173;ToolPublicationAdmissionStore:397,411 |
| `restore` M:1485 | B3 JournalHistoryQueries | RO | plain head→scope/storage-policy/read grant→activation projection;不加 FOR UPDATE | Controller:188;ToolPublicationAdmissionStore:282;ToolPublicationDataStore:950,2229;ManagedNativeJournalSnapshots:205,237,249,255,291 |
| `transactions` M:1560 | B3 | RO | plain head/read grant→先查 revision/bytes 再读 bounded payload;保留连续性/完整性与 workspace 检查 | Controller:209;ManagedNativeJournalSnapshots:129,131,132;ToolPublicationStore:559,561,562,566,587 |
| `recentTransactions` M:1644 | B3 | RC | 首次 restore/prefix 验证→反向分页两阶段读取→结束前再次 restore,不能改成 RR | Controller:221 |
| `commandTransaction` M:1700 | B3 | RC | 首次 restore/prefix→command hash/size/row→结束前再次 restore | Controller:232 |
| `hasPristineInitialJournal` M:1759 | B3 | NS | 全初始 journal/header/resource integrity 检查,开始和结束 head 一致;不获取 writer/不改变 fence;异常映射为 false | SessionRecoveryService:123 |
| `readResource` M:1920 | B5 JournalResourceReads | N | credential/head/read grant→retention read lease→resource FOR UPDATE→metadata/digest verify→更新 last_verified_at→可选 object IO/quarantine→lease check/close;不得迁入 HistoryQueries | ManagedToolResultProjector:176,318;ToolPublicationStore:630;ChildResultRelay:469,474,568,573,1161,1202,1212;SessionLifecycleCoordinator:937 |
| `readOwnerResource` M:1968 | B5 | P | 拒绝环境事务;非 CSI 回退 readResource;CSI 是短事务 admission→独立连接 fetch→短事务复核/receipt/lease 收尾,见第 6 节 | Controller:269 |
| `publishAgentInstructions` M:2328 | B4 JournalResourceWrites | T | scope→legacy CSI index locking probe→resource FOR UPDATE/插入或幂等验证;无 writer lease 新语义 | QwenHostedHarnessConnector:2111 |
| `publishAgentSkills` M:2341 | B4 | T | 同上,继续使用 AgentDefinitionExtensions.skillsBytes 的原字节编码 | QwenHostedHarnessConnector:2121,2145 |
| `publishInheritedExtensionResource` M:2350 | B4 | T | snapshot resource 拒绝→legacy probe→kind/schema/byte/digest→resource FOR UPDATE/插入或幂等 | QwenHostedHarnessConnector:2044,2049,2052 |
| `publishToolResult` M:2410,2416(4/5 参数) | B4 | T/T | authority guards→验证 bytes→physical P→Tg→placement→CSI continuation/R→H→no-lifecycle-claim(authority 也不能豁免 v2)→writer/native-head→resource→current fence→insert/replay→CSI time | Controller:198 |
| `hasLiveWriter` M:3459 | B1 | T | 直接 H FOR UPDATE,再 DB time;不补 root/tenant 锁,不改只读注解 | SessionLifecycleCoordinator:602,815,846 |
调用者扫描有两个名称相似但并非 Journal 调用,已排除:QwenHostedHarnessConnector:273 是 SDK client.releaseOwnership,:889 是 builder.approvalSelection。ToolPublicationAdmissionStore:295,342,361 和 ToolPublicationStore:493 的 `data.readResource` 也不是这个 Store。
### 4. 包内兼容入口完整矩阵
| 原入口 | 最终实现归属 | 事务/连接要求 | 调用者及保留点 |
| ---------------------------------------- | ---------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `usesDataSource` M:522 | facade 或 Rows 恒等委托 | N;`source != null && jdbc.getDataSource() == source` | ToolPublicationDataStore:96–99、ToolPublicationAdmissionStore:53–56、WorkspaceCsiApplicationHistory:60–61;不能改 equals/unwrap/连接池同 URL 判断 |
| `lockSnapshotWriter` M:577 | B1 WriterLifecycle | N,调用者已有事务 | ManagedNativeJournalSnapshots:89,108,179,200;原 P→lockPublicationWriter→ordinary admission→H→restore mapping;PublicationWriter/RestoreHead 保持值语义 |
| `lockPublicationWriter` M:598 | B1 WriterLifecycle | N,调用者已有事务 | ToolPublicationStore:327;ToolPublicationDataStore:1121,1181;ToolPublicationAdmissionStore:374;先 legacy index probe,再 physical P→Tg→placement→CSI/R→H→writer/native-head;不能忽略调用者之前的锁 |
| `readSnapshotResource` M:1925 | B5 ResourceReads | N;snapshots 自己已有隔离程序式事务 | ManagedNativeJournalSnapshots:520;updateVerification=false:resource plain SELECT,不更新 last_verified_at,不 quarantine,但仍持有/检查/关闭 retention lease |
| `requireCsiApplicationCredential` M:2581 | B1 Fencing | N,authority 同物理连接且已绑定 jdbc DataSource 事务 | WorkspaceCsiApplicationStore:105,119,137,184,213,230;先 authority.requireConnection,再 DataSourceUtils.isConnectionTransactional(target, same datasource),credential→isolated journal→R/H→CSI/token |
| `requireCsiHistoricalCredential` M:2601 | B1 Fencing | N,无 SQL/锁;校验 credential+retained token hash | WorkspaceCsiApplicationHistory:92,在其已有只读 RR snapshot 内调用;不能改成 live-writer/lease 检查 |
| `preflightCsiTerminal` M:2609 | B6 Commit | N,同 authority 连接;PreCut;bindings 必须同 DataSource | WorkspaceCsiApplicationStore:125,144;strict bytes→credential/isolation→commit validation→R/H→writer/head→terminalPreflight(target);返回原 TerminalCandidate |
| `commitCsiTerminal` M:2636 | B6 Commit | N,同 authority 连接,Stage.FROZEN | WorkspaceCsiApplicationStore:192;冻结字节与摘要匹配→重复 preflight→resource fence→journal fence→SEALED head CAS→最后 time check;不调用普通 commit,不新增 transport 通知 |
| `validateCsiTerminalBytes` M:2695 | B6 Commit 静态纯验证,facade 转发 | N,无 SQL/IO | WorkspaceCsiApplicationStore:336;保持 strict JSON 参数、exactBody 和同一验证规则 |
| `lockCsiOriginal` M:3162 | B1 Fencing | N;pinned CSI 必须原事务连接;ordinary standalone 仍可 null | ToolPublicationAdmissionStore:371;保留其先行 original-settled-result 及 tenant 顺序 |
| `toolResultLimit` M:2507 | Rows 的纯资源行约束,facade 保留静态转发 | N,无 SQL/IO | 现有包内静态入口;返回 kind 固定上限,verifyStoredResource 也使用 |
| `commandKeyHash` M:3832 | Rows 的键编码,facade 保留静态转发 | N,纯 bytes | ManagedAgentStore:3689;operation + NUL + commandId、UTF-8、SHA256 不变 |
| `journalCorrupt` M:3861 | Rows 行校验错误,facade 静态转发 | N | ToolPublicationStore:535,544,557,563;保留 HTTP/code/message |
| `skipContended` M:301 | B1 Fencing 静态工具 | 执行调用者传入 TransactionTemplate;只吞 tenant_guard_contended | ManagedProjectionResourceCollector:42;SessionResourceCollectionCollector:188;不强制 REQUIRES_NEW |
| `lockBounded` M:314 | B1 Fencing 静态工具 | 本身无事务;运行现有 Runnable | ManagedToolResultStore、ManagedProjectionResourceCollector、ToolPublicationDataStore/AdmissionStore/Store、SessionResourceCollectionCollector、ManagedAgentStore、ManagedUserMessageStore |
| `lockBoundedResult` M:321 | B1 Fencing 静态工具 | 同上 | ToolPublicationRetentionStore:156;仅 PessimisticLockingFailureException / QueryTimeoutException → tenant_guard_contended |
| `namedRootDeadlock` M:341 | B1 Fencing 静态工具 | 同上 | ToolPublicationRetentionStore:112;ManagedLegacySessionGuard:14;只转换 DeadlockLoserDataAccessException 或链中 MySQL 1213,普通 root wait 错误保留 |
还需保留 facade 上 `PublicationWriter` M:571、`TerminalCandidate` M:2577 两个既有包内 record,以及 `GUARD_ACQUIRE_TIMEOUT_SECONDS = 5` M:111。Root/retention 的已有外部调用会继续调用 facade 静态工具;静态转发不是新增实例依赖或 facade 回调。
### 5. B0 装配事实与迁移要求
| 原 setter | 引用及当前 Spring 条件 | standalone 默认 | 构造迁移要求 |
| -------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `configureHarnessRules` M:87 | HarnessRules,required @Autowired | null | Spring required 参数;保留现有 standalone null,不擅自加 SdkHarnessRules 默认 |
| `configureReaderVersion` M:92 | StorageReadPolicy,required @Autowired | StorageReadPolicy.inProcess() | Spring required;standalone 仍 inProcess |
| `setLifecycleExecution` M:205 | WorkspaceExecutionStore + SessionQueryStore,两参数的 `@Autowired(required=false)` 方法 | null/null | 任一 bean 缺失整个 setter 原本跳过;构造解析两可选参数后任一缺失必须归一化成 null/null,不能改成半启用或启动失败 |
| `setPublicationObjects` M:527 | ToolPublicationObjectStore,可选 | null | 构造可选,不创建对象存储默认 |
| `setToolResults` M:532 | ManagedToolResultStore,可选 | null | 构造可选,保留 capture 是否启用 |
| `setJournalEventTransport` M:539 | JournalEventTransport,可选、flag-off 无 bean | null | 构造可选,byte-identical flag-off;CSI isolation 会拒绝非空 transport |
| `setCredentials` M:546 | WriterCredentialPolicy,required @Autowired | WriterCredentialPolicy.unbound() | Spring 缺失仍 fail-closed,standalone 不新增 bound credential |
| `setCsiBindings` M:551 | RuntimeBindingRepository,可选 | null | 仅 instanceof JdbcRuntimeBindingRepository 且 `usesDataSource(jdbc.getDataSource())` 才保存;不匹配仍转成 null,不新增启动错误 |
以下构造器及八个 setter 原本均无声明式事务(N)。已有 3 个构造器 M:177/181/187:`(JdbcTemplate)` → 新建 extensionRecords;`(JdbcTemplate, extensionRecords)` → actions=null;Spring 当前 `(qualified JdbcTemplate, extensionRecords, actions)` 三者 required。三个构造链创建同一个 DataSourceTransactionManager,分别给 ToolPublicationRetentionStore 和 ownerReads(timeout=10);保留这两个原程序式事务对象的配置,不把它们换成新增 Spring Bean 或全局事务 manager 参数。
生产手工调用者只有两个:`server/.../maintenance/WorkspaceCsiRuntimeMain:106–107`(裸构造 + 同 datasource CSI bindings);`WorkspaceCsiWorkerAckMain:53–54`(裸构造 + SdkHarnessRules)。两者使用场景和默认值不同,不能用一个有额外 enabled 依赖的统一 fixture 替代。
手工测试的中心路径是 `VerificationFixtures.configure(ManagedSessionStore)` :40–46,目前只赋 HarnessRules / StorageReadPolicy。B0 应像 Agent wiring 那样改成明确构造工厂,避免 final 字段反射、先造后重建及 setter 变名。当前构造调用扫描覆盖 52 文件、150 处(2 处生产、148 处 verification),另有 11 文件、16 处显式 setter 调用。B0 迁移逐点核对构造参数、原 setter 条件及 fixture 创建顺序;实施时刷新清单,扫描结果归零只能证明旧装配调用已移除,不能代替行为验证。
扫描当前源文件未发现显式返回 ManagedSessionStore 的 `@Bean` 工厂;有 `ToolPublicationAsyncVerificationTest:317` 的 `.withBean(ManagedSessionStore.class, () -> fixture.sessions)` 路径,其 context 同时提供 HarnessRules、StorageReadPolicy、credentials、bindings 和对象存储。不能依赖 bean postprocessing 继续补已移除 setter,应迁移其已构造 fixture;B0 新增 @Bean 工厂回归验证,避免重演 #16061 漏洞。
依赖图审查:ManagedToolResultStore:40 依赖 EventProjectionStore(Agent facade),WorkspaceExecutionStore:38 依赖 jdbc/transactionManager/WorkspaceStorageGuard/settings;LiteTopicEventTransport:45 只依赖 producer/provider/topic。没有理由为 B0 增加新的 lazy 回环;Agent→records、skills→records 的既有 lazy 边仍原样保留。Spring constructor injection 的 full-context、事务代理和实际工厂测试仍必需。
### 6. 资源读取的实际副作用与事务
#### ordinary / snapshot
M:1930 共享实现先做 scope/credential/resource id/head/read grant;readGrant 自己查 retention live 和 DB now。`outputRetention.read`(ToolPublicationRetentionStore:379)用其现有 `TransactionTemplate`(默认 REQUIRED、DEFAULT、readOnly=false)执行 Tg→R→H 的 `lockGenericSession`,再插入 120 秒 `ma_read_lease`。这会加入既有环境事务;外层方法没有 NOT_SUPPORTED,不能新增。
ordinary 的 findResource `lock=true`,然后 verifyStoredResource,再写 last_verified_at(M:1953),TOOL_PUBLICATION 时读取 object stream;digest/length 错会 quarantine capture/resource。snapshot 的 findResource `lock=false`、不更新 verified、不 quarantine,但 retention lease 仍会插入、按需续期、检查和关闭。两者不是只读组件。`verifyStoredResource` 本身不做这些副作用,见第 8 节。
snapshot 调用者 `ManagedNativeJournalSnapshots:56–61` 既有 isolated template 为 REQUIRES_NEW + READ_COMMITTED;:89 admission/basis、:92 build、:94 lease close、:520 resource read 都沿用该模板。**必须保留这里已经存在的 REQUIRES_NEW**;“不引入 REQUIRES_NEW”只禁止新增,不能删除或复制到新组件。
#### owner
M:1978 同时拒绝 `isActualTransactionActive()` 或已绑定 jdbc datasource 的 resource。非 CSI(M:1995)仍回退 ordinary read。CSI 路径:
1. ownerReads template(M:197–200;同 datasource manager、REQUIRED/DEFAULT/readOnly=false、timeout 10 秒)开启 admission。lockCsiContinuation → original admission 或 DRAINING checkpoint continuation → R/H → read grant → resource metadata FOR UPDATE。校验 inline-only;生成 identity evidence → CSI/read-lease复核 → INSERT `CSI_RESOURCE_READ` OPEN receipt → 再复核 → INSERT ma_read_lease(generation=0)→保存回执。
2. 退出事务后 `fetchNativeResource` M:2172 使用 DataSource.getConnection() 自己取新连接,仅 SELECT 完整 inline bytes、statement timeout=10。这里才是独立读取阶段,不能持有 admission 事务或复用其物理连接。
3. `finishNativeRead` M:2199 再开 ownerReads 短事务:重锁 continuation/original→R/H→read receipt FOR UPDATE;身份/receipt变化直接拒绝。fetch 未知异常时记录 UNKNOWN / jdbc_fetch_unknown,原路径不删除 lease;正常返回时检查 read lease FOR UPDATE、metadata FOR UPDATE、row bytes/hash,复核 CSI/read grant,更新 verified,记录 RETURNED(verified 或具体拒绝码),最后复核后删 lease。某些早期 fencing 异常仍回滚而不强行结算,不能“优化”成 finally 删除。
4. fetch 的 RuntimeException/Error 路径尝试收尾,收尾异常作为 suppressed,原异常继续抛。不能把 owner IO 包成大事务,不能把多次 current-time/read-grant 合并成一次。
### 7. Commit 原子性、CSI 复核和通知
普通/lifecycle commit(M:1118–1459)完整保持一个入口事务。command replay(M:1176–1214)发生在新 resource/projection 写入之前;CSI replay 同样先核验当前 writer、activation replay 和 current time,ordinary replay 仍拒绝 v2 lifecycle claim。duplicate 路径不发事件。
新 commit 先 validateHead / generation / lease,再在需要时对 native `commitFileHistory`/`toolIntent`/`commitCheckpoint` 做 preflight,并重新读 DB time 检验 writer(M:1228–1245)。`commitResources` M:2809 每个 resource 在查 existing 之后、写资源之前执行传入 writeFence;这是已有调用内 callback,可原样保留,禁止把它改成构造器函数集合。
随后同事务完成 declared-resource census、checkpoint/history/hosted closure 校验、HostedCleanupReferences、lifecycle dispatch authorization(M:1313;用成对 execution/session 依赖)、extensionRecords.apply/applyNative、CSI acceptCommit + 再次 writer 检查(M:1343–1367)、actions.apply。journal INSERT 前 requireCsiWriterCurrent(M:1378),head UPDATE 前又 requireCsiWriterCurrent(M:1421),继续 capture tool result events、ManagedUserMessageStore.capture。不能把 resource/projection/journal/head 任一部分分成独立提交。
`publishJournalAfterCommit` M:1466 只由新 commit 尾部 M:1455 调用。transport null 不做事;无 synchronization(standalone)立即 publishCommitted;有 synchronization 注册 afterCommit。rollback 不通知,replay 不通知。CSI continuation/terminal 明确 `requireCsiJournalIsolation`(M:3245)拒绝 transport,因此 terminal 分支没有通知;不要为了统一 commit 加上通知。
CSI terminal 三个桥接方法使用 `WorkspaceCsiApplicationStore.tx` :402–410 创建的既有 10 秒事务和该事务物理连接的 bounded proxy。authority 持有的 connection identity、`DataSourceUtils.getTargetConnection` 和 `isConnectionTransactional` 不可替换为新连接、仅 same URL 判断或单独 helper transaction。preflight 还检查同 datasource JdbcRuntimeBindingRepository。commitCsiTerminal 在冻结字节比较后再次 preflight,每 resource 执行 authority time + writer fence,journal INSERT 前再 fence,head SEALED CAS 前再 fence,返回前再 authority time。`TerminalCandidate` 中 request/proof/token hash 原样保留。
### 8. 共享闭包与单向依赖
`JournalStoreRows` 的允许闭包限于原来的行访问、映射、读取时必要的完整性验证、固定键/字节约束,以及下表显式列出的窄行初始化。它不是纯只读组件,也不能扩展成通用业务 Support:
| 原方法 | 第一次需要 | 是否有 SQL/副作用 | 迁移裁定 |
| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| headMapper/transactionMapper/resourceMapper,HeadRow/TransactionRow/ResourceRow | B1 writer 的 head 与 release receipt | 行映射;不写业务状态 | Rows |
| findHeadForUpdate/requireHeadForUpdate M:3119/3359;requireHeartbeatHead M:3140;requireHead M:3367;databaseNow M:3474 | B1 | 前两者保留 R→H 和 live read;heartbeat 只 H;plain head 不锁;databaseNow 是 SELECT | Rows;不能因统一 helper 加锁或丢锁 |
| findResource M:3393/3397、requireResourceScope M:3787、requireTransactionWorkspace M:3798 | B1 release receipt | 默认 FOR UPDATE,显式 false 才 plain;scope 检查无写入 | Rows;保留 lock 参数各路径值 |
| verifyStoredResource M:3575 | B1 releaseOwnership M:997 | 状态/长度/SHA256/metadata;TOOL_PUBLICATION 用 SELECT COUNT 验证 capture 对应,inline managed-tool-outcome 用 SELECT COUNT 检查 quarantine;**没有 UPDATE、对象 IO、租约或 quarantine 写入** | 随 B1 迁入 Rows 的 resource-row 完整性边界,后续 B3/B4/B5/B6 复用,不能复制 |
| toolResultLimit M:2507 | verifyStoredResource 内部调用 M:3618 | 纯 kind→固定上限 switch;无 SQL/IO | 同上;facade 静态入口保留 |
| activationLeaseRow M:1512、ActivationLeaseRow、restoreHead M:1495 | B1 renewWriter M:863、snapshot bridge M:591 | 只 SELECT + strict JSON/digest + head/activation 投影一致性校验,无更新/IO/lease操作 | 随 B1 放 Rows;B3 公开 history 稍后迁移 |
| requireHeadScope M:3742 | B1 | scope/state/counter/digest 检查并调用 StorageReadPolicy.requireReadable;不写 SQL | Rows 行合法性;必须保留 reader policy 实际读取时点,不能构造期预先计算 |
| transactionRow M:3409、storedTransaction M:3434 | B3 或 B6 首次需要时 | 行映射、byte/hash verify、Base64编码,无写入 | Rows;不提前构建框架 |
| storedResource M:2520 | B6 commit closure reader | findResource(FOR UPDATE)+state/scope/verify+DTO,无 object IO/lease | B6 首次需要时迁入 Rows;不调用 B5 公共 readResource,避免额外 lease、last_verified_at/quarantine |
| ensureSessionRoot M:3818 | B1 acquire / B2 lifecycle | R 锁 + INSERT IGNORE ma_event_stream | 选定为 Rows 内显式的窄行初始化 helper,只供现有 acquire/lifecycle 路径调用。保留调用时点、R 锁和 event-stream INSERT,不扩成通用 mutation,不让 B2 回调 facade 或 WriterLifecycle。 |
Fencing(B1)承接 credential、writer/read-grant、placement、CSI continuation/current-time/lease/history-owner 和锁错误映射;`checkLifecycleWriter` M:266、`requireNoLifecycleClaim` M:482、`requireOrdinaryAdmission` M:490、lifecycleFenced/protocolFenced M:498/510 是被 writer/resource/commit 共享的底层 gate,可随首次使用进入 Fencing。B2 LifecycleAuthorization 承接 3 个业务授权入口及 commit 的“新 lifecycle effect 是否可派发”策略,不回调 WriterLifecycle。`ensureSessionRoot` 已选定归 Rows 的**明确窄行初始化**,只服务现有两个入口,DAG 保持单向;不要为此建 Support,也不要把 writer 状态机放进去。
最终实例 DAG:Rows 在底层;Fencing→Rows;WriterLifecycle→Rows/Fencing;LifecycleAuthorization→Rows/Fencing(及已有 execution/session 成对依赖);HistoryQueries→Rows/Fencing;ResourceWrites→Rows/Fencing;ResourceReads→Rows/Fencing/既有 retention+object store;Commit→Rows/Fencing/LifecycleAuthorization/ResourceWrites(及已有 extensionRecords/actions/toolResults/transport)。Commit 内 reader callback 直接调用 Rows 的 storedResource,writeFence 是调用内已有回调。每个新对象由 facade 构造一次、字段 final,不传 facade,不引入新 Spring Bean。外部静态锁工具回转现有 facade 兼容符号不形成新的实例回调。
commitResources 是 B4 的 commit 内资源写入方法;ValidatedResource 可用同包的窄值类型或嵌套包内值类型共享,保持现有数据形状。validateCommit 是 B6 的协议/完整提交校验,不因共享资源值类型提前迁入通用 Rows。B4 提取时既有 facade commit 直接调用 ResourceWrites 完整资源闭包,不允许 ResourceWrites 再调用 facade 取查询/锁工具。
+202
View File
@@ -0,0 +1,202 @@
# Activity view
[English](activity-view.md) | [简体中文](activity-view.zh-CN.md)
## Problem and evidence
The sidebar lists projects and conversations but offers no single queue for
approval, input, errors, unread updates and running work. Completion dots are
currently ephemeral. The reference desktop application implements an activity
snapshot rather than continuously sorting a filtered list. Its bundled code
was decompiled and inspected alongside the supplied report and screenshots; it is behavioral
reference material, not a dependency or source to copy.
## Scope and decisions
Implement a complete activity mode for the Web Shell's daemon workspace
conversations. Respect the existing host, locked-workspace and trust boundaries.
The activity directory follows the selected daemon host; it does not aggregate
other hosts or standalone conversations without a workspace. Managed-agent and
live-voice surfaces keep their own navigation. Unsupported sources must not be
silently treated as workspace conversations. Qwen has no
equivalent of ChatGPT's account history or explicit silent-heartbeat flag; do
not invent those sources or suppress ordinary background work.
Activity is an alternative mode of the existing conversation surface, not an
additional list beside it. While open, it replaces the ordinary workspace
navigation, session search and conversation groups in that surface, including
the rail layout's Home column. The top-level New session action remains outside
that surface and stays available when enabled by the host. Close activity with
the bell or Cmd/Ctrl+Alt+U to restore ordinary navigation; Escape also closes it
when a dialog or menu does not own that key.
The entry requires the daemon's existing `session_organization` capability,
the default conversation surface (no host-supplied `sessionContent`), and no
special section view. There is no additional read-state capability requirement.
Without these conditions the activity entry and shortcut are unavailable and
the existing navigation remains in use.
Use the existing directory, live-state feed, transcript page reads and archive
APIs. No new directory database, model-generated summaries or per-row polling.
Reuse the sidebar's existing in-memory `completedUnreadIds` for unread state.
A conversation becomes unread when the page observes its running-to-finished
transition while it is not the current conversation. Activity rows and the
existing blue dots share this state; historical conversations are not initially
unread merely because they lack a read record. Do not add backend read fields,
capabilities or write requests. Refreshing or remounting the sidebar clears the
record, completions while the page is closed cannot be reconstructed, and
separate tabs or clients do not synchronize reads. These are intentional limits
of preserving the existing completion-dot semantics. Preview reads do not
change unread state.
## Comparison with the earlier persisted-read design
The current design preserves the existing completion-dot behavior. It reuses
the daemon's existing organization capabilities and introduces no read-state
backend version gate. Marking read is a local state update, so it has no receipt
request failures, retries or cross-client synchronization races. Its limits
are deliberate: refresh loses unread records, offline completions cannot be
reconstructed, and each tab or client has independent read state.
The earlier design added `lastReadActivityAt` and required the new
`session_activity_read_state` capability and an upgraded daemon. It could retain
read state across reloads and share it through directory refresh. It used the
observed `updatedAt`, with monotonic writes to prevent older requests from
overwriting newer acknowledgements. Those guards did not make a session update
equivalent to a completed assistant turn, and persistence alone did not recover
every offline completion. Treating historical conversations with no receipt as
unread also changed the old behavior undesirably. Receipt failures, retries and
late responses added asynchronous state that this revision removes.
The retained optional `hasTurnError` live-state field is separate from unread
state. Older daemons can still support activity through existing capabilities,
but without this field error presentation is best effort: an error may be absent
or remain stale until other session data refreshes. A future persisted design
could improve continuity if it defines completion or turn identities, migration
of unknown historical read state, and whether read ownership belongs to a user
or a shared daemon. Those decisions are outside this revision.
## Interaction and state
- A bell and Cmd/Ctrl+Alt+U toggle activity. Escape closes it. The bell indicates
unread or actionable states, not running work alone.
Its selected state uses the existing theme blue with a faint blue background,
consistently in the expanded sidebar and navigation rail. Both entries use a
pointer cursor in their selected and unselected states.
- The list starts with its first category, without a separate Activity heading.
Exactly one options trigger belongs to the first displayed category: Priority
when enabled, otherwise Pinned or the first recent date. An empty list uses a
Today heading so the options remain reachable. The options trigger reuses the
existing workspace overflow button, vertical ellipsis, right inset and
standard auto-width dropdown rather than activity-specific menu styling.
Category headings use the normal sidebar's spacing, typography and controls.
Activity participates in the existing session-list layout without an extra
padded container; groups reuse the normal pinned-session list so headings,
row titles, status indicators and action buttons align with normal mode.
Expanded empty categories use that same list container and bottom spacing;
the priority empty-state message is rendered inside it.
Pinned shares its expanded
state and chevron interaction with the normal sidebar. Priority also has a
collapse toggle, local to the open activity view; date headings remain static.
Collapsed category rows are not rendered, do not load previews and consume no
visible-row budget. Pinned uses the normal sidebar's ordering, with newly
pinned conversations appended rather than placed first.
- Initial priority order is input/approval/error, unread, running, then idle;
ties use descending activity time and stable identity. Child sessions are
excluded unless pinned; archived and inaccessible sessions are excluded.
- Opening records a snapshot. Existing priority entries retain their relative
order and remain after reading or completion; new eligible entries merge in.
Closing discards the snapshot. Clear read removes settled read entries only.
- Priority is enabled by default, a separate pinned section and scheduled tasks
are disabled. Preferences persist. Reset restores these defaults.
The options menu begins with a Display label. When preferences differ from the
defaults, an accessible reset icon appears at the right of that label instead
of a separate reset menu row. Reset keeps the menu open and recomputes the
activity snapshot; it does not change read state or pinned expansion.
- Recent conversations cover seven local calendar days including today, grouped
by date. Priority has no age cutoff. Sections never duplicate identities.
A shared budget starts at ten and expands by ten.
- Rows use the normal sidebar's `renderSessionRow` directly, preserving its
selection, keyboard handling, status indicators, inline actions, context menu,
rename, pin and archive behavior with the same capability checks. There is no
activity-specific row layout, styling, idle dot or always-visible workspace
suffix. The existing hover details popover shows time, workspace and other
available context, including branch, pull requests and session identity.
Assistant previews are capped at 600 Unicode code points and use the normal
row's existing snippet slot and styles. Visible rows load bounded transcript
pages without attaching or starting sessions. Preview failures use the shared
notice layout and can be retried; loading and partial failure remain
distinguishable from an empty queue.
- Selecting a conversation clears its existing completion dot through the
sidebar's shared state. Mark all read removes the priority section's unread
identities from that same in-memory set; it neither clears the snapshot nor
changes unrelated conversations. Closing activity discards only its snapshot,
not the sidebar's unread set.
- Bulk archive applies to priority, excluding recent and pinned-only entries.
Confirmation explains when running work will stop. The existing atomic
close-and-archive API performs the operation; successes disappear, failures
remain, and undo restores successful archives without restarting work.
Operations are bounded to eight concurrent requests. Navigation leaves an
archived current conversation only after confirmed success.
Pending operations, result messages and the undo offer belong to the sidebar,
so closing and reopening activity does not discard them, including when a
request finishes while the view is closed. Changing the host/workspace scope
discards this operation state; late results from an earlier scope cannot
populate the new scope, even after switching back. List suppression and
restoration overlays remain local to the activity view and follow the
authoritative directory when it remounts.
## Implementation boundaries
Unread state stays in the existing sidebar component; the activity model accepts
the derived unread flag and the catalog controller handles directory reads,
previews and archive operations. Each identity has one completion observer:
only while activity is open do its covered conversations transfer to the
activity observer and leave the legacy scans. With activity closed, the existing
ordinary-sidebar observers retain their visibility and freshness rules. This
preserves normal collapsed-rail reminders and prevents duplicate observers from
recreating a completion already marked read.
Core organization metadata, Bridge summaries
and daemon organization routes gain no read-state fields. Live-state includes
turn errors. Existing workspace routes must use resolved runtime storage,
including failure paths. Web Shell adds an activity model, controller and view
under `components/sidebar`, reuses shared UI primitives, and integrates the bell
with the sidebar. The activity view owns grouping, budgets and preview loading;
the sidebar supplies its existing session renderer, so ordinary row features
and styling have one implementation. English and Chinese messages remain
aligned.
## Validation and acceptance
Test ranking, snapshot stability, date boundaries, filtering, identity,
pagination, first-category menu placement and empty fallback, priority collapse,
shared pinned collapse state and ordering, collapsed-row budget, normal/activity
header and row alignment, shared row actions and hover context, reset icon
visibility and keyboard access, observed
completion transitions, shared blue-dot/read behavior,
refresh reset, absence of read-state writes, stale async responses, partial
failures, archive scope and undo. Verify that activity replaces workspace/search
navigation, preserves the enabled top-level New session action, and restores the
ordinary surface through each exit; also verify the `session_organization`
capability gate. Run a baseline against global `qwen`
and browser fixtures before implementing, then focused tests, browser E2E,
build, typecheck and bundle. Read the full diff in two clean audit passes and
perform an independent review. Test only isolated conversations; never archive
the user's real reference chats. There are no product decisions awaiting user
approval; implementation and verification findings may refine this document.
## Related work and visual evidence
Issue [#16020](https://github.com/QwenLM/qwen-code/issues/16020) tracks this
feature. Existing unread persistence work in
[#11451](https://github.com/QwenLM/qwen-code/issues/11451) and
[#12672](https://github.com/QwenLM/qwen-code/pull/12672) remains separate; this
feature does not resolve offline or reload continuity. The latter proposes
browser localStorage, not backend receipts, and overlapping sidebar changes
will need reconciliation if it lands first.
The screenshots below use synthetic daemon fixtures, not personal conversations.
![Ordinary sidebar](images/activity-view/normal.png)
![Activity sidebar](images/activity-view/activity.png)
+143
View File
@@ -0,0 +1,143 @@
# 查看活动
[English](activity-view.md) | [简体中文](activity-view.zh-CN.md)
## 问题与证据
侧栏目前展示项目和会话,但缺少统一处理审批、输入、错误、未读更新和运行中任务的
入口。完成提示点只保存在内存中。参考桌面应用使用活动快照,而不是持续重排的过滤
列表。已结合交接报告和截图反编译并检查其打包代码;这些材料仅作为行为参考,不作为依赖,
也不直接复制代码。
## 范围与决策
为 Web Shell 的 daemon 工作区会话实现完整活动模式,遵守已有主机、锁定工作区和
信任边界。活动目录跟随当前选中的 daemon 主机,不聚合其他主机或无工作区的独立
会话。Managed-agent 和实时语音保留自身导航。不把不支持的来源偷偷当成工作区
会话。Qwen 没有 ChatGPT 账号历史或明确的静默心跳标记,因此不虚构这些来源,也不
隐藏普通后台任务。
活动是既有会话区域的替代模式,不是在旁边增加另一份列表。打开时,它替换该区域中的
普通工作区导航、会话搜索和会话分组,包括 rail 布局的 Home 列。顶部“新任务”操作
位于该区域之外,在宿主启用时仍可使用。点击铃铛或按 Cmd/Ctrl+Alt+U 关闭活动后恢复
普通导航;没有对话框或菜单占用 Escape 时,也可用 Escape 退出。
入口要求 daemon 已有的 `session_organization` 能力、默认会话区域(宿主没有提供
自定义 `sessionContent`),且当前不是特殊分区视图。不额外要求已读状态能力。
不满足这些条件时,活动入口及快捷键不可用,继续使用原有导航。
复用既有目录、实时状态、会话记录分页和归档接口,不新增目录数据库、模型摘要或
逐行轮询。未读状态复用侧栏原有的内存集合 `completedUnreadIds`。页面观察到非当前
会话从运行中变为结束时,将它记为未读。活动条目和原有蓝点共享这份状态;历史会话
不会因为没有已读记录而默认未读。不新增后端已读字段、能力声明或写入请求。刷新
页面或重新挂载侧栏会清空记录,无法还原页面关闭期间的完成情况,不同标签页或客户端
之间也不同步已读。这些限制是保留原有完成提示点语义的明确选择。预览读取不改变
未读状态。
## 与此前持久化已读方案的比较
当前方案保留原有完成提示点的行为,复用 daemon 已有的会话组织能力,不新增已读状态
所需的后端版本门槛。标为已读只更新本地状态,不涉及回执请求失败、重试或跨客户端
同步竞争。其限制是明确的:刷新后丢失未读记录,无法还原离线期间的完成情况,每个
标签页或客户端独立维护已读状态。
此前方案增加了 `lastReadActivityAt`,要求新增的 `session_activity_read_state`
能力及升级后的 daemon。它可以在刷新后保留已读状态,并通过目录刷新共享状态。
回执使用已观察到的 `updatedAt`,通过单调写入防止旧请求覆盖较新的确认。但这些保护
并不能让会话更新等同于助手回合完成,单靠持久化也无法恢复所有离线完成事件。将没有
回执的历史会话视为未读,还不必要地改变了原有行为。回执失败、重试和延迟响应增加了
异步状态处理,本次修订将这些逻辑移除。
保留的可选实时状态字段 `hasTurnError` 与未读状态无关。旧 daemon 仍可凭已有能力
支持活动视图,但缺少该字段时错误展示只能尽力提供:错误可能未显示,或直到其他
会话数据刷新前仍然过期。未来如果重新设计持久化,可以通过明确完成事件或回合身份、
未知历史已读状态的迁移,以及已读状态属于用户还是共享 daemon,改善跨页面连续性。
这些决策不在本次修订范围内。
## 交互与状态
- 铃铛和 Cmd/Ctrl+Alt+U 切换活动模式,Escape 关闭。铃铛提示未读或需要处理的状态,
单纯运行中不会点亮关注提示。
激活时使用既有主题蓝色图标和淡蓝背景,展开侧栏与导航栏入口保持一致。
两个入口在激活和未激活时均使用手形指针。
- 更多入口复用工作区的竖向三点图标、按钮和右侧间距,下拉菜单使用普通菜单的
自适应宽度,不定义活动专用的更多菜单样式。
- 列表直接从首个分类开始,不单独显示固定的“活动”标题。选项按钮只出现一次,位于
首个显示分类的标题上:启用优先事项时位于优先事项,否则位于置顶或首个最近日期。
空列表使用“今天”标题,确保选项仍可访问。选项按钮复用既有工作区菜单的竖向省略号、
右侧缩进和标准自适应宽度下拉菜单,不使用活动专用菜单样式。分类标题沿用普通侧栏
的间距、文字样式和控件。活动视图直接参与既有会话列表布局,不增加带内边距的外层
容器;分组复用普通置顶会话列表,使标题、行文本、状态标识和操作按钮与普通模式
对齐。展开的空分类也使用同一列表容器及底部间距,优先事项的空状态提示放在该容器内。
置顶与普通侧栏共享展开状态及箭头交互。优先事项同样支持折叠,其展开状态
保留在本次打开的活动视图内;日期标题不折叠。折叠后的分类行不渲染、不加载预览,
也不占用可见行预算。置顶沿用普通侧栏的排序,新置顶会话追加在末尾,而非排在最前。
- 初始优先顺序为输入/审批/错误、未读、运行中、空闲,同级按活动时间降序和稳定身份
排序。子会话除非置顶否则排除;归档和无权访问的会话排除。
- 打开时建立快照,已有优先条目保持相对顺序,阅读或完成后暂留,新符合条件的条目
增量合并。关闭清除快照。“清除已读”仅移除已经结束且已读的条目。
- 默认显示优先事项,不单独显示置顶,不包含定时任务。偏好持久化,可恢复默认。
选项菜单顶部显示“显示”标签。偏好偏离默认值时,标签右侧出现具有无障碍名称的
重置图标,不再单独占用重置菜单行。重置后菜单保持打开并重新计算活动快照,不改变
已读状态或置顶展开状态。
- 最近会话包含今天在内的七个本地自然日,按日期分组。优先事项不受时间窗口限制。
各分区不重复。共享首屏预算为十条,每次增加十条。
- 会话行直接复用普通侧栏的 `renderSessionRow`,保留相同能力检查下的选择、键盘、
状态标识、行内操作、上下文菜单、重命名、置顶和归档行为。不另建活动行布局或
样式,不添加空闲状态点,也不常驻显示工作区后缀。时间、工作区和其他可用上下文
通过既有悬浮详情展示,包括分支、PR 和会话身份。助手预览最多 600 个 Unicode
码点,复用普通行已有的摘要槽位及样式。可见行按需读取有界会话记录页,不附加或
启动会话。预览失败复用通用提示布局并支持重试;加载中和部分失败不能伪装成空列表。
- 选择会话时,通过侧栏共享状态清除原有完成提示点。“全部标为已读”从同一内存集合
中移除优先分区的未读身份,不清除快照,也不改变无关会话。关闭活动视图只丢弃
活动快照,不清空侧栏的未读集合。
- 批量归档仅作用于优先事项,不影响最近会话和仅在置顶分区的条目。确认框明确提示
运行中的工作将停止,复用现有原子关闭并归档接口。成功项移除,失败项保留,撤销
恢复成功归档的会话但不重启任务。最多八个请求并发。当前会话确认归档成功后才
导航离开。
进行中的操作、结果提示及撤销入口由侧栏持有,因此关闭并重新打开活动不会丢失,
请求在关闭期间完成时也如此。切换主机或工作区作用域会丢弃这些操作状态;即使随后
切回原作用域,之前请求的延迟结果也不能写入新作用域。列表临时隐藏和恢复覆盖仍
属于活动视图本地状态,重新挂载时以实际目录为准。
## 实现边界
未读状态保留在既有侧栏组件中,活动模型接收由其派生的未读标记,目录控制器负责目录
读取、预览和归档。每个会话身份只有一个完成状态观察器:仅活动打开时,其覆盖的会话
才交给活动观察器并退出原有扫描。活动关闭时,普通侧栏观察器继续沿用原有可见性与
数据新鲜度规则,保留正常折叠条提醒,并避免双观察器重新标记已经清除的未读。
Core 组织元数据、Bridge 摘要和
daemon 组织修改路由不增加已读
字段。实时状态补充回合错误。既有工作区路由的所有路径(包括失败)都使用已解析
运行时的存储。Web Shell 在 `components/sidebar` 下新增活动模型、控制器和视图,
复用共享 UI 基础组件,并在侧栏接入铃铛。活动视图负责分组、预算和预览加载,侧栏
提供既有会话行渲染器,使普通行的功能与样式只有一份实现。中英文文案保持一致。
## 验证与验收
覆盖排序、快照稳定性、日期边界、过滤、身份、分页、首分类菜单位置与空态回退、优先
事项折叠、共享置顶折叠状态与排序、折叠行预算、普通与活动模式的标题及行对齐、共享
行操作与悬浮上下文、重置图标显示及键盘访问、观察到的
完成状态转换、蓝点与
已读行为共享、刷新重置、不发送已读写入、过期异步结果、部分失败、归档范围和撤销。
验证活动会替换工作区及搜索导航、保留已启用的顶部“新任务”操作,并通过各退出方式
恢复普通区域;同时验证 `session_organization` 能力门槛。
实现前对全局 `qwen` 和浏览器夹具记录基线;
实现后运行针对性测试、浏览器 E2E、build、typecheck 和 bundle。完整 diff 进行两轮
连续干净自查,并独立审查。仅操作隔离测试会话,不归档用户真实参考聊天。目前没有
等待用户批准的产品决策;实现和验证结果可以进一步修订本文。
## 相关工作与界面证据
Issue [#16020](https://github.com/QwenLM/qwen-code/issues/16020) 跟踪本功能。
既有未读持久化工作
[#11451](https://github.com/QwenLM/qwen-code/issues/11451) 和
[#12672](https://github.com/QwenLM/qwen-code/pull/12672) 保持独立;本功能不解决
离线或刷新后的连续性。后者提出浏览器 localStorage 方案,而非后端读回执;若其先合入,
需要协调重叠的侧栏改动。
以下截图使用模拟 daemon 数据,不包含个人会话。
![普通侧栏](images/activity-view/normal.png)
![活动侧栏](images/activity-view/activity.png)
@@ -0,0 +1,59 @@
# Channel Request Owner Approval
[English](channel-request-owner-approval.md) | [简体中文](channel-request-owner-approval.zh-CN.md)
## Problem
Text permission commands currently require shared-session operator privileges
before locating the request. An admitted user can therefore start a turn and
receive a tool confirmation they cannot answer, including in a direct message
using `sessionScope: "single"`. Group request owners encounter the same failure.
## Decision
Allow an admitted sender to use `/approve` (allow once) and `/deny` for their
own pending request in their current attended turn. This applies to direct and
group conversations, independently of the `operators` list, including an empty
list. Ownership of one request does not grant shared-session management rights.
Retain the existing operator requirements for `/approve-always`, approvals of
another sender's text request, `/cancel`, `/clear`, `/who`, `/status`, `/loop`,
`/btw`, loop tools, and steering. Existing native-card owner checks remain in
place. This change does not broaden native-card delegation or persistent grants.
## Implementation and Boundaries
Keep session routing and the shared-session operator predicate unchanged. Record
the active run ID with a pending permission. For a caller without shared-session
operator rights, filter lookup candidates to requests whose sender, current
attended run, chat, and thread match the caller. A missing, cancelled,
unattended, or superseded run cannot supply owner authority. Apply this filter
before producing ambiguity lists or permission titles, including explicit IDs.
Operators retain the existing lookup rules and normal conversation admission.
Named-task selection, native-card ownership, permission options, and settlement
continue to use the existing paths. Do not infer operators from pairing or
automatically rewrite configuration.
## Validation and Acceptance
- In `single`, `chat_thread`, and group `thread` sessions, the current request
owner can approve once or deny without operator membership.
- A real privately paired sender can answer their own direct-message request.
- Another ordinary member cannot answer or list another sender's requests; the
denied attempt leaves the request available for its owner.
- Cross-chat and cross-thread explicit IDs, requests without an attended owner,
cancelled turns, and requests belonging to an earlier run are rejected.
- A non-operator owner cannot gain persistent permission with `/approve-always`
in a shared session or clear the shared conversation.
- Existing operator delegation and native-card request settlement keep working.
- Run focused and full ChannelBase tests, build, typecheck, and bundle; distinguish
local harness checks from live IM acceptance, which requires a deployed build.
## Risks
This intentionally separates request authority from session management: an empty
`operators` list continues to disable shared-session management and delegation,
but no longer prevents an attended request owner from approving once or denying.
The request must use the active run's sender snapshot, rather than the initial
owner of a `single` session shared across senders.
@@ -0,0 +1,48 @@
# Channel 请求发起者审批
[English](channel-request-owner-approval.md) | [简体中文](channel-request-owner-approval.zh-CN.md)
## 问题
文字权限命令目前在查找请求前就要求共享会话操作员权限。因此,通过准入的用户可以
发起轮次并收到工具确认,却无法回答,使用 `sessionScope: "single"` 的私聊也会
遇到此问题。群聊中的请求发起者同样受影响。
## 决策
允许通过准入的发送者对自己当前交互轮次的待审批请求使用 `/approve`(仅允许
本次)和 `/deny`。该规则覆盖私聊和群聊,不依赖 `operators` 名单,空名单也适用。
拥有某个请求的审批权,不代表获得共享会话管理权。
`/approve-always`、代答其他发送者的文字请求、`/cancel`、`/clear`、`/who`、
`/status`、`/loop`、`/btw`、loop 工具及 steer 保留现有操作员权限要求。
现有原生卡片的发起者校验保持不变,不扩大卡片代审或持久授权权限。
## 实现及边界
保持会话路由和共享会话操作员判定不变。在待审批请求中记录当前 run ID。
对于没有共享会话操作员权限的调用者,查找时仅保留发送者、当前交互轮次、聊天及
线程均匹配的请求。缺失、已取消、无人值守或已被替换的轮次不能提供发起者权限。
在生成歧义列表或显示请求标题之前应用筛选,显式请求 ID 也遵循此规则。
操作员保留现有查找规则,并仍需通过正常会话准入。命名任务选择、原生卡片归属、
权限选项和请求结算复用已有路径。不从 pairing 推导操作员,也不自动改写配置。
## 验证与验收
- 在 `single`、`chat_thread` 和群聊 `thread` 会话中,当前请求的发起者无需
操作员身份即可仅批准本次或拒绝。
- 真实通过私聊配对的发送者能够回答自己在私聊中触发的请求。
- 其他普通成员不能回答或列出别人的请求;拒绝代答后,请求仍可由发起者回答。
- 拒绝跨聊天或线程的显式 ID、没有交互发起者的请求、已取消轮次和旧轮次请求。
- 共享会话中的非操作员发起者不能通过 `/approve-always` 获取持久权限,也不能
清空共享对话。
- 现有操作员代审和原生卡片请求结算继续正常工作。
- 运行定向及完整 ChannelBase 测试、构建、类型检查和打包;区分本地测试与需要
部署新构建后才能进行的真实 IM 验收。
## 风险
本次有意区分请求审批权和会话管理权:空 `operators` 继续禁用共享会话管理和
代审,但不再阻止当前交互请求的发起者仅批准本次或拒绝。请求必须使用当前轮次的
发送者快照,不能使用跨发送者共享的 `single` 会话最初记录的归属。
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

+74
View File
@@ -0,0 +1,74 @@
# Add a sidebar session to Split View
[English](sidebar-session-split-view.md) | [简体中文](sidebar-session-split-view.zh-CN.md)
## Problem and current behavior
Issue #14775 requests an “Add to Split View” action in a sidebar session's
context menu. Today users must open Split View from the footer, then use its
session picker. App owns the pane list; SplitView already supports adding
sessions, deduplication, and a six-pane limit.
## Proposed changes
- Sidebar adds a dropdown-only `split` action to its default action list,
with English and Simplified Chinese labels. It follows the existing action
allowlist, screen-size capability, busy state, and workspace eligibility
checks. Only default workspace sessions (including legacy sessions without
source metadata) offer the action, matching the split session directory.
Archived, channel, standalone, Live, unknown, and untrusted sessions do not
offer this action.
- App appends the selected session when Split View is open. When it is
closed, the menu opens the selected session alongside the current workspace
session when available, without restoring the old pane list. The footer
retains its existing behavior of restoring that list.
Opening reuses the existing split opener and classification. Additions made
while opening is pending are combined within the current classification
generation; navigation cancels that pending selection. Appending
uses the same synchronous pane update as the existing picker: Sidebar
already limits targets to eligible active workspace sessions. This
preserves existing panes and unsent attachments. Embedded hosts
controlling the pane list receive the existing change callback and retain
ownership of applying the change. Pending additions include only IDs the host
has already accepted through its controlled props.
- Repeated sessions are not duplicated. At six panes, adding another session
is disabled with the existing limit explanation; existing panes are kept.
- SplitView reveals a newly added external pane by clearing maximization
and making that pane active, matching its existing session picker.
## Design decisions and scope
Keep Sidebar → App → SplitView ownership intact. The menu emits an intent;
App combines IDs; SplitView renders and manages pane focus. Reuse the
existing workspace classification, providers, Radix menu, and portal.
No daemon, SDK, desktop Rust, CSS, keyboard shortcut, transport, or archive
lifecycle changes are required. The desktop app loads the same Web Shell.
Existing split transport limitations are outside this feature.
## Affected files
- `packages/web-shell/client/components/sidebar/WebShellSidebar.tsx`
- `packages/web-shell/client/App.tsx`
- `packages/web-shell/client/components/SplitView.tsx`
- `packages/web-shell/client/i18n.tsx`
- Adjacent component tests and Web Shell browser tests.
## Validation and acceptance criteria
Verify the real menu opens current + selected sessions, appends without
replacing panes, ignores duplicates, and respects the six-pane limit.
After leaving a full split, the menu must still open current + selected.
Verify the new pane is visible after another pane was maximized. Check
delayed standalone classification, pane identity, and consecutive additions,
including the first opening, deduplication, capacity, and navigation cancellation.
Check English/Chinese labels, action allowlists, archived and ineligible sessions,
trusted secondary workspaces, small screens, controlled embedding, and the
existing footer/picker behavior.
Run focused unit and browser tests, build, typecheck, bundle, and repeat the
relevant tests in local Docker Linux. Record baseline and verification
evidence in `.qwen/e2e-tests/14775/`.
## Open questions
None. Duplicate selection is a no-op; capacity does not evict existing panes.
@@ -0,0 +1,63 @@
# 从侧边栏将会话添加到分屏视图
[English](sidebar-session-split-view.md) | [简体中文](sidebar-session-split-view.zh-CN.md)
## 问题与当前行为
Issue #14775 希望在侧边栏会话菜单中增加“添加到分屏视图”。
目前用户必须先通过底部入口打开分屏,再使用会话选择器。
App 持有面板列表;SplitView 已支持添加会话、去重和六个面板的上限。
## 拟议改动
- Sidebar 默认动作列表新增仅在下拉菜单出现的 `split` 动作,
提供英文和简体中文文案。沿用现有动作白名单、屏幕大小能力、
忙碌状态与工作区资格检查。只允许普通工作区会话(包括缺少来源元数据
的旧会话),与分屏会话目录一致。归档、渠道、独立、Live、未知工作区及
不可信工作区的会话不提供此动作。
- App 在分屏打开时追加选中会话;分屏关闭时,菜单打开选中会话,
并在当前会话属于工作区时将其一起加入,不恢复旧面板列表。
底部入口仍保持恢复旧列表的既有行为。打开时复用现有分屏打开流程与会话分类。
等待打开期间的添加操作合并到当前分类代次的选择中;导航离开会取消待处理选择。
追加时沿用面板内选择器的同步面板更新:Sidebar 已将目标限制为符合资格
的活跃工作区会话,因此可以保持现有面板及未发送附件。
对于控制面板列表的嵌入宿主,通过现有变更回调提出请求,
仍由宿主决定何时应用变更。等待分类期间只合并宿主已通过受控属性接受的会话 ID。
- 重复会话不会重复添加。已有六个面板时禁用添加新会话,
并显示现有限制说明;保留已有面板。
- SplitView 收到外部新增面板时退出最大化并激活新面板,
与现有会话选择器的行为一致。
## 设计决策与范围
保留 Sidebar → App → SplitView 的职责分工。菜单发送操作意图,
App 组合会话 ID,SplitView 渲染并管理面板焦点。
复用已有工作区分类、provider、Radix 菜单及 portal。
无需修改 daemon、SDK、桌面 Rust、CSS、快捷键、传输机制或归档生命周期。
桌面端加载同一个 Web Shell。已有分屏传输限制不属于本功能范围。
## 涉及文件
- `packages/web-shell/client/components/sidebar/WebShellSidebar.tsx`
- `packages/web-shell/client/App.tsx`
- `packages/web-shell/client/components/SplitView.tsx`
- `packages/web-shell/client/i18n.tsx`
- 相邻组件测试和 Web Shell 浏览器测试。
## 验证与验收标准
通过真实菜单验证当前与选中会话一起打开、追加时不替换已有面板、
重复添加无效以及六面板上限。验证另一面板最大化后新增面板仍可见。
离开已有六个面板的分屏后,菜单仍应打开当前与选中会话。
验证独立会话分类延迟、面板身份保持以及连续追加,包括首次打开期间的
连续添加、去重、容量限制及导航取消。
检查中英文文案、动作白名单、归档及不符合资格的会话、可信次工作区、
小屏幕、受控嵌入,以及原有底部入口和选择器行为。
运行定向单元测试和浏览器测试、构建、类型检查、打包,并在本地
Docker Linux 中重跑相关测试。基线与验证结果保存在
`.qwen/e2e-tests/14775/`。
## 未决问题
无。重复选择无操作;达到容量上限时不移除已有面板。
+1 -1
View File
@@ -10,7 +10,7 @@ The session Token Usage panel repeats metrics in tall cards, making comparison d
Place the session total above a rounded two-column overview, with “Total tokens” inline after the number and refresh/time on the right, including when the header wraps. Use foreground text for the total label with at least 4.5:1 text contrast. The primary columns are Input and Total output. Use fixed 28px text for the session total and 24px for primary overview values. Input retains the complete normalized count without subtracting cached tokens; its caption shows cache count and cached/input percentage. Output retains a reasoning-count caption. Preserve zero values. Use normal 12px foreground sans-serif captions and omit inclusion notices.
Sort models by normalized total consumption in descending order. Use fixed 15px bold names, 14px shares and gradient bars with the palette from the user's reference image. Omit rank tiles, per-model token-total lines, click disclosures and row-hover backgrounds. One shared Tooltip provider groups the panel so moving promptly between rows skips another 300ms delay. Below the name, an always-visible three-column grid shows per-model API timing inline, because comparing these values across models is a primary reason to open the panel. Cells flow in order: average first-token latency and, when generation duration is positive, output speed and average generation time for timed streams, followed by request count and average latency. Models without streamed timing show only request count and average latency in the first row. Keep the original arithmetic and availability conditions. Grid labels use foreground mixed 70% with the card so they reach at least 4.5:1 contrast; values use fixed 13px monospace and stay on one line. Labels and values retain their full localized text in HTML title attributes so ellipsized text remains available at narrow panel widths. Use local foreground colors for authored focus indicators (including Refresh) so they contrast at least 3:1 against their adjacent card in light and dark themes; keep the global theme tokens unchanged.
Sort models by normalized total consumption in descending order. Use fixed 15px bold names, 14px shares and solid bars in the model's color; [Token usage trend and model colors](token-usage-trend.md) defines the palette, the success rate and the speed timeline. Omit rank tiles, per-model token-total lines, click disclosures and row-hover backgrounds. One shared Tooltip provider groups the panel so moving promptly between rows skips another 300ms delay. Below the name, an always-visible three-column grid shows per-model API timing inline, because comparing these values across models is a primary reason to open the panel. Cells flow in order: average first-token latency and, when generation duration is positive, output speed and average generation time for timed streams, followed by request count and average latency. Models without streamed timing show only request count and average latency in the first row. Keep the original arithmetic and availability conditions. Grid labels use foreground mixed 70% with the card so they reach at least 4.5:1 contrast; values use fixed 13px monospace and stay on one line. Labels and values retain their full localized text in HTML title attributes so ellipsized text remains available at narrow panel widths. Use local foreground colors for authored focus indicators (including Refresh) so they contrast at least 3:1 against their adjacent card in light and dark themes; keep the global theme tokens unchanged.
Above each bar, show “Input xxx (cache rate: xxx) · Total output xxx”. Hide the separate cache count and reasoning from the inline row. Keep it on one line with ellipsis on overflow. Hover, keyboard focus and touch always expose input, cache count with ratio, output and reasoning, one complete metric per tooltip line, whether the row fits or is clipped. Use an ordinary arrow cursor for the metric row. Put the consumption share at the far right of the track; hovering, focusing or tapping the track shows “Token share: percentage”. Tapped share tooltips remain open until dismissal; their noninteractive content does not intercept taps on adjacent metric rows. Keep actual shares, including an over-reported source above 100%, without silently clamping the displayed number. Keep the bar row's top margin, model-list gap and model-heading bottom margin at zero. All fonts remain fixed during sidebar resizing. By model, By agent and Tools headings share the same size, weight and color.
+1 -1
View File
@@ -10,7 +10,7 @@
会话总量放在圆角两列摘要卡片上方,“总 Token”文案跟随数值同行显示并使用正文颜色达到至少 4.5:1 的文字对比度,右侧保留刷新及更新时间,换行后仍右对齐。摘要使用输入、总输出两列;会话总量固定 28px,摘要主数值固定 24px。输入保留归一化后的完整输入量,不减去缓存,下方显示缓存数量及缓存量/输入量比例;总输出下方显示思考数量。保留零值,小字使用 12px 普通无衬线字体及正文颜色,不显示包含关系提示。
模型按归一化总消耗降序排列。名称固定 15px 加粗,占比固定 14px,比例条沿用用户参考图配色。不显示序号色块、每个模型的总量行、点击展开或整行 hover 背景。面板统一使用一个 Tooltip provider,首次提示延迟 300ms,随后跨行查看即时打开。模型名下方用始终可见的三列网格展示该模型的 API 耗时指标,直接展示数值,因为跨模型对比这些数值是打开面板的主要目的之一。单元格依次排列:平均首 token 延迟、流式计时生成时长为正时的输出速度和平均生成耗时,随后是请求次数和平均延迟。没有流式计时的模型只在第一行显示请求次数和平均延迟。保持原计算方式和可用条件。网格标签使用正文色与卡片底色按 70% 混合,达到至少 4.5:1 的对比度;数值固定 13px 等宽字体并保持单行。标签和数值的 HTML title 属性保留完整本地化文本,使窄面板下被省略的内容仍可查看。自定义焦点环(包括刷新按钮)使用局部正文颜色,在浅色与深色主题下均与相邻卡片达到至少 3:1 的对比度,不修改全局主题 token。
模型按归一化总消耗降序排列。名称固定 15px 加粗,占比固定 14px,比例条使用该模型颜色的纯色;配色、成功率和速度时间线见[Token 消耗趋势与模型颜色](token-usage-trend.zh-CN.md)。不显示序号色块、每个模型的总量行、点击展开或整行 hover 背景。面板统一使用一个 Tooltip provider,首次提示延迟 300ms,随后跨行查看即时打开。模型名下方用始终可见的三列网格展示该模型的 API 耗时指标,直接展示数值,因为跨模型对比这些数值是打开面板的主要目的之一。单元格依次排列:平均首 token 延迟、流式计时生成时长为正时的输出速度和平均生成耗时,随后是请求次数和平均延迟。没有流式计时的模型只在第一行显示请求次数和平均延迟。保持原计算方式和可用条件。网格标签使用正文色与卡片底色按 70% 混合,达到至少 4.5:1 的对比度;数值固定 13px 等宽字体并保持单行。标签和数值的 HTML title 属性保留完整本地化文本,使窄面板下被省略的内容仍可查看。自定义焦点环(包括刷新按钮)使用局部正文颜色,在浅色与深色主题下均与相邻卡片达到至少 3:1 的对比度,不修改全局主题 token。
比例条上方显示“输入 xxx(缓存率:xxx) · 总输出 xxx”,行内不显示单独的缓存数量和思考。指标保持单行,溢出时显示省略号。无论是否截断,悬停、聚焦或触屏点击始终显示完整的输入、缓存数量及缓存率、总输出、思考,每项各占一行。指标行使用普通箭头光标。消耗占比位于线条最右侧,悬停、聚焦或触屏点击进度条显示“Token 占比:百分比”。触屏提示在主动关闭前保持显示,提示内容不拦截对相邻指标行的点击。保留真实占比,即使异常来源超过 100% 也不静默截断显示数值。比例条行的顶部 margin、模型列表 gap 和模型标题底部 margin 均为零。拖动侧栏宽度时字号固定;“按模型”“按agent”“工具”标题使用一致的字号、字重及颜色。
+79
View File
@@ -0,0 +1,79 @@
# Token usage trend and model colors
[English](token-usage-trend.md) | [简体中文](token-usage-trend.zh-CN.md)
## Status and scope
Implemented in two steps:
- **Step 1 (#16108, frontend only):** request success rate, session output speed, a categorical model palette and a per-model share bar.
- **Step 2 (this change):** per-request timing samples and failure records in core and on the daemon stats route, plus a speed and first-token timeline in Web Shell Token Usage.
This design builds on [Per-model generation timing](model-generation-timing.md) and [Token usage hierarchy](token-usage-hierarchy.md). CLI `/stats` is out of scope.
## Problem
1. Each model showed only its average output speed. A slowdown in the middle of a session, a few slow outliers, and a steady low speed all produced the same number.
2. The daemon counted failed API calls per model (`api.totalErrors`), but the panel never showed them. A model that kept hitting 429s or timeouts looked healthy.
3. Models were hard to tell apart. The first two palette colors were both blue (`#4b67ff` / `#4f94e7`, OKLab ΔE 12.3 for normal vision, below the 15 floor). Every share bar faded from the same blue, and the cards had no other color.
## Design
### D1 — Success rate (step 1)
Success rate = `(totalRequests − totalErrors) / totalRequests`, for the session and per model. Every failed attempt counts, retries included, which matches the existing counter. Internal prompts stay included, as in `totalRequests`. A tooltip on the label states this. Rates below 100% never round up to 100%: anything between 99.95% and 100% shows as 99.9%. Status: 100% shows a check, below 100% a warning, below 90% an error. Each status comes with an icon and text, never color alone, and uses the theme's `--success-color`, `--warning-color` and `--error-color`.
### D2 — Model colors (step 1)
Six categorical colors with separate steps for light and dark: blue, orange, aqua, violet, magenta, yellow. Light: `#2a78d6 #eb6834 #1baf7a #6250d6 #e87ba4 #eda100`. Dark: `#3987e5 #d95926 #199e70 #9085e9 #d55181 #c98500`. Adjacent pairs keep OKLab ΔE ≥ 9 under protanopia, deuteranopia and tritanopia, and ≥ 15 for normal vision. A model's slot is the order it was first used, which is the daemon's key order for `models`, not its rank by tokens. A model therefore keeps its color when the ranking changes. The seventh and later models share a neutral gray instead of a generated hue. Color marks identity only: share bar segments, the legend swatch, the card's left strip, and the timeline's dots and lines. All text keeps the text colors.
### D3 — Per-request samples in core (step 2)
`StatsModelMetrics` gains two optional fields, filled only in session buckets that feed the stats route:
- `timingSamples`: one entry per timed, non-internal response, under the same rules as `generation`: `{ at, ttftMs, generationMs, outputTokens, source? }`, oldest first. Only the latest `MAX_TIMING_SAMPLES` (200) are kept.
- `failures`: `byKind` counts every failed call by HTTP status code, else error type, else `unknown`. `recent` holds the latest `MAX_FAILURE_SAMPLES` (50) as `{ at, kind, source? }`. `byKind` is a null-prototype map, and stays one after snapshot/restore.
`at` comes from the event's `event.timestamp`, which falls back to the current time. Session replay feeds stored `ui_telemetry` events back through `addEvent`, so a resumed session rebuilds its timeline from history. Events recorded before `generation_ms` existed still carry no timing, as before. As with `generation`, neither field is added to `models`, which is also written to managed usage records with closed key sets.
### D4 — Stats route fields (step 2)
`GET /session/:id/stats` copies `timingSamples` (when non-empty) and `failures` into `models[name]`. `ServeSessionStatsModelMetrics` and `DaemonSessionStatsModelMetrics` declare both as optional. Older daemons omit them, and the panel then shows no timeline or spread. A full poll every 2 s carries at most 200 timing and 50 failure entries per model.
### D5 — Speed timeline (step 2)
When any model has samples or failures, a "Speed over time" card appears between the overview and "By model":
- The x-axis is wall-clock time. Each request is a dot in its model's color, and a line traces the median of its five neighbors to show the trend without following every outlier.
- A toggle switches the y-axis between output speed (tok/s) and first-token latency (s). The chart has one axis at a time. Responses whose generation window is below 100 ms appear in the first-token view only.
- Failed requests appear as red × marks in a strip under the plot, in both views.
- Hovering or touching shows the nearest request in a tooltip, with crosshair and highlight: model, time, subagent, speed, first token, output tokens and generation time; for a failure, its kind.
- With two or more models, a legend doubles as a series toggle. Hiding the last visible series shows all again.
- The SVG's accessible name summarizes the metric and each visible model's median.
### D6 — Card spread and failure kinds (step 2)
Each model card with at least two rate-measurable samples shows a dot strip of every sample's output speed. It has a p10–p90 band and a p50 tick, on one scale shared by all cards, followed by `p50 · p10–p90 tok/s`. The existing average output speed in the timing grid stays. A model with failures lists them by kind, for example `Failed: 429 ×2 · timeout ×1`, most frequent first.
## Compatibility
- All new fields are optional and additive. Older daemons render as before step 2.
- The timeline holds the most recent 200 timed responses per model. Older ones still count toward every aggregate.
- A managed session restored through `restoreSessionModelMetrics` does not get samples back, the same as per-model timing.
## Out of scope
- Separating retries from final failures, and excluding internal prompts from the success rate.
- Incremental (`since`) polling of samples.
- Timelines in CLI `/stats`.
## Validation
- **Core:** samples are recorded with event time, subagent source and the 200 cap; untimed, internal and session-less events are skipped; failures are keyed by status code, then error type, then `unknown`, including `__proto__`, and capped at 50 while the counts keep growing; `byKind` stays null-prototype after snapshot/restore.
- **CLI:** the stats extension method returns `timingSamples` and `failures`, and omits them when empty.
- **Web Shell unit:** dots per model in each view, the 50 ms window excluded from speed, failure marks, the accessible summary, legend toggling, nearest-point and failure tooltips, card spread quantiles, failure kinds in Chinese, and no timeline without samples.
- **Web Shell E2E (Chromium):** 1200px/390px, en/zh-CN, light/dark. The timeline renders with the model colors, the tooltip opens on hover and tap, and the ×-mark count matches the failures.
## Open questions
None.
+79
View File
@@ -0,0 +1,79 @@
# Token 消耗趋势与模型颜色
[English](token-usage-trend.md) | [简体中文](token-usage-trend.zh-CN.md)
## 状态与范围
分两步实现:
- **第一步(#16108,只改前端):** 请求成功率、会话输出速度、模型分类色和按模型的占比条。
- **第二步(本次改动):** core 和 daemon stats 路由提供逐次请求的耗时样本和失败记录,Web Shell Token 消耗面板新增输出速度与首 token 时间线。
本设计基于[按模型的生成耗时](model-generation-timing.zh-CN.md)和 [Token 消耗层级](token-usage-hierarchy.zh-CN.md)。CLI `/stats` 不在范围内。
## 问题
1. 每个模型只显示平均输出速度。会话中途变慢、少数几次特别慢、始终偏慢,这三种情况得到的是同一个数字。
2. daemon 一直按模型统计失败的 API 调用(`api.totalErrors`),但面板从未显示。一个模型反复遇到 429 或超时,看起来也一切正常。
3. 多个模型很难区分。调色板前两个颜色都是蓝色(`#4b67ff` / `#4f94e7`,色觉正常时 OKLab ΔE 只有 12.3,低于 15 的及格线)。所有占比条都从同一个蓝色开始渐变,卡片上也没有其他颜色。
## 设计
### D1 — 成功率(第一步)
成功率 = `(totalRequests − totalErrors) / totalRequests`,会话整体和每个模型各算一个。每次失败的尝试都计入,包括重试,与现有计数口径一致。内部请求仍然计入,和 `totalRequests` 一致。标签的提示里写明了这一口径。低于 100% 的值不会四舍五入成 100%:介于 99.95% 和 100% 之间的显示为 99.9%。状态:100% 显示 ✓,低于 100% 显示警告,低于 90% 显示错误。每种状态都同时有图标和文字,不只靠颜色区分,颜色使用主题的 `--success-color`、`--warning-color` 和 `--error-color`。
### D2 — 模型颜色(第一步)
6 个分类色,浅色和深色主题各有一套:蓝、橙、青绿、紫、粉、黄。浅色:`#2a78d6 #eb6834 #1baf7a #6250d6 #e87ba4 #eda100`;深色:`#3987e5 #d95926 #199e70 #9085e9 #d55181 #c98500`。相邻两色在红色盲、绿色盲和蓝色盲下 OKLab ΔE ≥ 9,色觉正常时 ≥ 15。模型的颜色按首次使用的顺序分配(即 daemon 返回的 `models` 键顺序),不按 token 排名分配,因此排名变化时颜色不变。第 7 个及以后的模型共用一个中性灰色,不再生成新颜色。颜色只用来标识模型,出现在占比条分段、图例色块、卡片左侧色条,以及时间线的点和线上。所有文字仍使用正文颜色。
### D3 — core 中的逐次请求样本(第二步)
`StatsModelMetrics` 新增两个可选字段,只写入供 stats 路由使用的会话桶:
- `timingSamples`:每次有计时数据、非内部的响应记一条,规则与 `generation` 相同:`{ at, ttftMs, generationMs, outputTokens, source? }`,按时间从旧到新,只保留最近 `MAX_TIMING_SAMPLES`(200)条。
- `failures`:`byKind` 按 HTTP 状态码统计所有失败调用,没有状态码时用错误类型,都没有时记为 `unknown`。`recent` 保留最近 `MAX_FAILURE_SAMPLES`(50)条,格式为 `{ at, kind, source? }`。`byKind` 是无原型的 map,快照/恢复后仍是。
`at` 取自事件的 `event.timestamp`,没有时取当前时间。会话回放会把存储的 `ui_telemetry` 事件重新传给 `addEvent`,所以恢复的会话会从历史记录重建时间线。`generation_ms` 字段出现之前记录的事件仍然没有计时数据,与之前一样。和 `generation` 一样,这两个字段都不加到 `models` 上,因为 `models` 也会写入键集合固定的 managed 用量记录。
### D4 — stats 路由字段(第二步)
`GET /session/:id/stats` 把 `timingSamples`(非空时)和 `failures` 复制到 `models[name]`。`ServeSessionStatsModelMetrics` 和 `DaemonSessionStatsModelMetrics` 把这两个字段声明为可选。旧版 daemon 不返回它们,面板就不显示时间线和分布条。每 2 秒一次的全量轮询中,每个模型最多带 200 条计时样本和 50 条失败记录。
### D5 — 速度时间线(第二步)
只要有任一模型有样本或失败记录,就在概览和「按模型」之间显示一张「速度趋势」卡片:
- 横轴是实际时间。每次请求是一个点,颜色与所属模型一致;一条线连接相邻 5 次的中位数,反映趋势而不随每个离群点波动。
- 切换按钮在输出速度(tok/s)和首 token 延迟(秒)之间切换纵轴,图表任何时候只有一个纵轴。生成窗口不足 100 ms 的响应只出现在首 token 视图中。
- 失败的请求在图表下方的一条带上用红色 × 标出,两个视图都显示。
- 鼠标悬停或触摸时,提示框显示最近的那次请求,同时显示十字线和高亮:模型、时间、子 agent、速度、首 token、输出 token 和生成耗时;如果是失败的请求,则显示失败类型。
- 有两个及以上模型时,图例同时是显示/隐藏开关。隐藏最后一个可见的模型时,所有模型重新显示。
- SVG 的无障碍名称概括当前指标和每个可见模型的中位数。
### D6 — 卡片上的分布条和失败类型(第二步)
至少有两个可计算速率样本的模型卡片会显示一条分布条:每个样本的输出速度是一个点,并标出 p10–p90 区间和 p50 刻度,所有卡片使用同一刻度;后面跟着 `p50 · p10–p90 tok/s`。计时网格中原有的平均输出速度保留不变。有失败记录的模型按类型列出失败次数,例如 `失败:429 ×2 · timeout ×1`,次数多的在前。
## 兼容性
- 所有新字段都是可选、增量的。旧版 daemon 下的显示与第二步之前相同。
- 时间线只保留每个模型最近 200 次计时响应,更早的响应仍计入所有汇总值。
- 通过 `restoreSessionModelMetrics` 恢复的 managed 会话不会恢复样本,和按模型的计时数据一样。
## 不在范围内
- 区分重试和最终失败,以及从成功率中排除内部请求。
- 样本的增量(`since`)轮询。
- CLI `/stats` 中的时间线。
## 验证
- **core:** 样本记录事件时间、子 agent 来源,并遵守 200 条上限;跳过没有计时数据的事件、内部请求和没有会话 ID 的事件;失败依次按状态码、错误类型、`unknown` 分类(包括 `__proto__`),`recent` 保持 50 条上限而计数继续增长;`byKind` 在快照/恢复后仍是无原型的。
- **CLI:** stats 扩展方法返回 `timingSamples` 和 `failures`,为空时省略。
- **Web Shell 单测:** 两个视图中每个模型的点数、50 ms 窗口不计入速度、失败标记、无障碍摘要、图例切换、最近点和失败的提示框、卡片分布条的分位数、中文失败类型,以及没有样本时不显示时间线。
- **Web Shell E2E(Chromium):** 1200px/390px、中英文、浅色/深色。时间线按模型颜色渲染,悬停和点击时提示框打开,× 标记的数量与失败数一致。
## 未决问题
无。
+5 -2
View File
@@ -43,7 +43,8 @@ that never changes between rounds should be configured once.
unset, so the skill keeps reading the variables it already documents and a
one-off export still wins.
- The skill states the three-tier order — flag, variable (export, `.env`, or
settings), default — and keeps publishing consent per invocation.
settings), default — and leaves publishing consent to the skill's
session-consent rule, never to a coordinate.
## Design decisions
@@ -72,7 +73,9 @@ that never changes between rounds should be configured once.
- The bridge runs from `loadSettings` where the environment is applied. Callers
that skip environment loading are per-request config reads and do not need
it.
- Coordinates are not consent: `--publish` still gates every comment, and the
- Coordinates are not consent: every comment still requires an explicit
publish request — given in the round's invocation or inherited within the
same session as the skill's **Manual re-verification** defines — and the
pre-post guard and read-back are unchanged.
## Validation plan
+2 -2
View File
@@ -20,7 +20,7 @@ Status: implementing with this change.
- 把 `verifyPr` 列入 workspace 受限根设置:合并期剥离 workspace 值、告警说明其被忽略。设置路由本就不把这些键暴露给 workspace 写入,因此真正保护取值的是剥离而不是路由守卫。
- 把 `QWEN_VERIFY_ASSETS_REPO` 与 `QWEN_VERIFY_ASSETS_BRANCH` 加入"项目 `.env` / `settings.env` 不得提供"的排除集。两者都由仓库控制,而环境加载器先于 settings 桥接执行;若不排除,仓库仍可通过自己 export 变量重定向目标。启动环境、home 作用域 `.env` 与 `verifyPr.*` 仍可设置它们。
- 启动时 CLI 在这两个变量未被设置的前提下,把它们写入 `QWEN_VERIFY_ASSETS_REPO` / `QWEN_VERIFY_ASSETS_BRANCH`;skill 继续读它已经文档化的变量,一次性的 export 仍然优先。
- skill 明确三级顺序——flag、变量(export / `.env` / settings)、默认值——并保持发布同意按次给出。
- skill 明确三级顺序——flag、变量(export / `.env` / settings)、默认值——发布同意则交由 skill 的会话同意规则管辖,坐标绝不构成同意。
## 设计决策
@@ -33,7 +33,7 @@ Status: implementing with this change.
- 写在 workspace 设置里的值不会生效。这是设计目的,但必须可见:schema 描述与 skill 都写明,避免维护者去排查一个被静默忽略的键。
- 通过 `settings.env` 设置这两个变量在任何作用域都不生效(含 User),并在 stderr 明确提示改用 `verifyPr.*` 或启动环境。合并后的 map 无法区分 workspace 条目与 operator 条目,所以只能采用"不区分作用域"这一条可审计的规则;用户级通道是专用设置,`~/.qwen/.env` 仍然可用。
- 桥接位于 `loadSettings` 应用环境的位置。跳过环境加载的调用方都是按请求读取配置,不需要它。
- 坐标不是同意:`--publish` 仍然门控每条评论,发布前 guard 与发布后回读均不变。
- 坐标不是同意:每条评论仍须有显式发布请求——在本轮调用中给出,或按 skill 的 **Manual re-verification** 在同一会话内继承——发布前 guard 与发布后回读均不变。
## 验证计划
+11 -3
View File
@@ -78,9 +78,10 @@ them rather than at the end of the document.
when every call succeeded. `--compare --start <a> --now <b>` reports every
reason the round is dirty and exits 1.
The checker gains two rules over the artifacts the contract asks the round to
write. The artifact contract names both, because a file no artifact list asks
for is a file no round produces, and a check over an absent file is silent:
The checker gains three rules over the artifacts the contract asks the round
to write. The artifact contract names all three, because a file no artifact
list asks for is a file no round produces, and a check over an absent file is
silent:
- `assertions.json` carries `failures: [{ id, attribution }]` whenever `fail` is
nonzero, one entry per failed assertion, attributed `pr`, `harness` or
@@ -93,6 +94,13 @@ for is a file no round produces, and a check over an absent file is silent:
<target>` and
`rows[].commit`. A listed commit with no row is an error naming the commits,
and so is an empty list: a claim over no commits is vacuously true.
- `assertions.json` may carry `gates: [{ suite, pass, fail, skipped? }]`, one
entry per existing test suite the round ran as a targeted gate. Those counts
stay out of `pass`/`fail`/`total`, which count only the checks the round
wrote, each suite run counting as one, and the publisher prints their
totals on a line of their own. A malformed list is an error. Over 1000
assertions with no gate results is a warning: the four such rounds among
18 recent CI artifacts had each counted whole workspace suites.
Each prepared artifact directory keeps `context.json` with the round's full
coordinates — `repo`, `pr`, `baseRefOid` and `headRefOid` — so a local round
+3 -2
View File
@@ -28,10 +28,11 @@
- `scripts/verify-pr-check.mjs <dir>` 把发布侧的规则套到本地产物目录:必需文件、四个判定词、为正且自洽的断言三元组、以及不允许带失败断言的 `merge-ready`。报告形态与证据命名只作为警告,因为纯文档轮次没有截图,检查器也无法知道是否跑过 harness。
- `scripts/verify-pr-guard.mjs` 接管发布前守卫的两个半程。`--pr <n> --repo <owner>/<repo> --snapshot <file>` 抓取 head、PR 状态以及三个评论列表(带 `--paginate`),只有每次调用都成功才写文件。`--compare --start <a> --now <b>` 列出本轮 dirty 的全部原因并退出 1。
检查器在产物契约要求轮次写出的产物上新增两条规则。产物契约里两者都要点名:产物清单没要求的文件,轮次就不会产出,而对一个不存在的文件做检查是静默的。
检查器在产物契约要求轮次写出的产物上新增三条规则。产物契约里三者都要点名:产物清单没要求的文件,轮次就不会产出,而对一个不存在的文件做检查是静默的。
- `assertions.json` 在 `fail` 非零时带 `failures: [{ id, attribution }]`,每条失败断言一项,归因为 `pr`、`harness` 或 `environment`。若 `findings`/`blocked` 的账本里没有任何一条归因为 `pr`,报错;账本长度与 `fail` 不一致,同样报错。有失败但没有账本只报警告,因此旧产物仍能通过。
- `mutations.json` 可以声明 `coverageClaim: "per-commit"`,并给出来自 `git log --no-merges <previous>..<head> --not <target>` 的 `productionCommits` 与 `rows[].commit`。列出的提交没有对应行时报错,并点名这些提交;列表为空同样报错——对空集合做的声明恒真。
- `assertions.json` 可以带 `gates: [{ suite, pass, fail, skipped? }]`,轮次作为定向门禁运行的每个已有测试套件一项。这些计数不进 `pass`/`fail`/`total`——后者只统计本轮自己写的检查,每次套件运行算其中一条;发布端把门禁合计单独印成一行。列表格式不对报错。断言超过 1000 条却没有记录任何门禁结果只报警告:最近 18 份 CI 产物里这样的 4 轮都把整个 workspace 的测试套件算了进去。
每个已准备的产物目录保存带完整坐标的 `context.json`——`repo`、`pr`、`baseRefOid` 与 `headRefOid`——本地轮次因此从一个文件读出两个 OID,而不是从 `HEAD` 推导。prepare 只在当前树是 `headRefOid` 的干净检出时才写它:轮次复用这棵树作为 A/B 的 head 侧,所以 HEAD 不对会退出 3 并点名要跑的 `git checkout`,树脏会退出 3 并要求维护者自己提交或 stash——脚本绝不代为 stash,且拒绝时不创建任何产物目录。检查器优先使用该轮快照,并注明实际使用的来源。旧目录回退到按 PR 共享的快照;该轮快照不可读或无效时只报警告,不会改用后续轮次的共享提交号。
@@ -89,7 +90,7 @@
- `check` 拒绝带失败断言的 `merge-ready`、不一致的三元组、未知判定词;对相同计数下的 `findings` 予以通过。
- 当失败账本里没有任何一条归因为 PR 时,`check` 拒绝 `findings` 与 `blocked`;账本长度与 `fail` 不一致时拒绝;对带 `harness` 失败的 `inconclusive` 予以通过;旧产物没有账本时只报警告。
- `check` 拒绝所列提交未被行全覆盖的 `per-commit` 声明,缩写与完整 OID 双向匹配;对空提交列表上的声明同样拒绝,而没有提出声明时保持静默。
- skill 的产物契约会要求这两条规则读取的两个文件;其枚举命令用 `--not <target>` 减去目标分支,而不是在第一个父提交处截断;任一处被改掉都有 pin 变红。
- skill 的产物契约会要求这些规则读取的两个文件;其枚举命令用 `--not <target>` 减去目标分支,而不是在第一个父提交处截断;任一处被改掉都有 pin 变红。
- 任一调用失败时 `guard --snapshot` 不写文件并退出 1;`guard --compare` 在快照畸形、列表变短、条目新增/被编辑/消失、head 移动、状态变化时退出 1,两侧是同一个文件时退出 2,只有一切未动才退出 0。列表变短时报它自己的原因加一个丢失条数;长度相等时仍逐条点名消失的 id。
- 条目正文变了而时间戳没变、或 review 的 state 变了时,`guard --compare` 报 dirty;条目不带 body、或 review 不带 state 的列表,`guard --snapshot` 直接拒绝。
- 模型、CLI 版本、skill 哈希三者都解析成功时,发布的报告评论以署名页脚收尾;否则以通用签名收尾;弱通知——包括产物下载成功但没有报告——一律保留通用签名。lane 在基线重新钉住之后、agent 启动之前采集三个值,该顺序有元测试钉住。skill 与发布侧携带同一页脚模板并有元测试钉住,`report.md` 本身不带页脚。
@@ -21,22 +21,26 @@ override; narrow desktop windows keep desktop editing.
Remove the legacy touch action grid and simulated Tab/Esc/left/right keys. Keep add,
model, approval, dictation, and send controls. Hide the width toggle on touch;
retain the context-usage control when enabled by the host. Put workspace
selection and Git controls
in a separate context row so starting a task in a particular workspace or
branch remains possible. The row's measured height, two lines when it wraps,
is added to the composer's height cap, so it does not come out of the
attachments' room when a soft keyboard shrinks the viewport. The rows stack
without overlapping touch targets.
selection and Git controls on the leading side of the editing row, with hide
keyboard and expand on the trailing side. Long workspace names truncate within
the available space; multiple context controls may wrap within their group.
Retain the group's measured height allowance in the composer's height cap, so
context controls and wrapping host actions do not come out of the attachments'
room when a soft keyboard shrinks the viewport.
Context and editing controls must not overlap.
Preserve host-provided toolbar slots.
Use 44px touch targets and wrap controls on very narrow viewports rather than
clip them. Keep Plan as an active-state chip. While a turn runs, stop remains
clip them. Center toolbar icons in their targets and remove extra gaps within
the mobile toolbar groups, including the context-usage button. Keep Plan as an
active-state chip. While a turn runs, stop remains
available alongside send when there is a draft; stopping never clears that
draft. Voice capture retains its existing reduced toolbar and hides the context and
editing rows.
Add a small editing row with expand and, while the textarea is focused, hide
keyboard. Its leading slots stay free unless there is something contextual to
keyboard. Its leading slots contain workspace/Git controls when present and
otherwise stay free unless there is something contextual to
show: the labelled history button when the host has no Add entry, and the
Shell toggle. Recall goes through the history search panel, which lists and
filters past inputs. One-entry-at-a-time stepping is not offered on touch,
@@ -16,16 +16,20 @@
### 工具栏与编辑
移除旧触屏操作网格和模拟 Tab、Esc、左右方向键。保留添加、模型、审批、语音输入及发送控件。
触屏下隐藏宽度切换;保留宿主启用的上下文占用控件。工作区选择和 Git 控件放到独立上下文行,
保留在指定工作区、分支中开始任务的能力。上下文行按实测高度(换行时为两行)计入输入框高度上限,
软键盘缩小视口时不再挤占附件空间;各行上下排列,触控区域不重叠。保留宿主自定义工具栏插槽。
触屏下隐藏宽度切换;保留宿主启用的上下文占用控件。工作区选择和 Git 控件放到编辑行左侧,
收起键盘和展开按钮放在右侧。长工作区名称在可用空间内省略,多个上下文控件可在组内换行。
保留输入框高度上限中该组实测高度的补偿,软键盘缩小视口时,
上下文控件及换行的宿主操作不挤占附件空间。
上下文控件与编辑控件的触控区域不重叠。保留宿主自定义工具栏插槽。
触摸区域为 44px;极窄视口允许换行,不裁剪控件。Plan 保持开启状态标签。
触摸区域为 44px;极窄视口允许换行,不裁剪控件。工具栏图标在触控区域内居中,
移除移动端工具栏各组内部的额外间距,包括上下文占用按钮。Plan 保持开启状态标签。
任务运行时,有草稿也同时提供停止和发送;停止不清空草稿。
语音录制继续使用现有精简工具栏,并隐藏上下文行与编辑操作行。
增加小型编辑操作行,提供展开和输入框聚焦时的收起键盘按钮。
行首位置只在有上下文内容需要展示时才占用:宿主未启用 Add 时显示带标签的历史按钮,以及 Shell 开关。
行首优先显示工作区/Git 控件;没有这些控件时保持留空,除非需要展示其他上下文内容:
宿主未启用 Add 时显示带标签的历史按钮,以及 Shell 开关。
回看历史统一走历史搜索面板,可列出并过滤以往输入。触屏不再提供逐条翻阅——
纯 textarea 的恢复路径无法重建内联标签 chip。
极窄屏允许操作行换行,行内控件随输入框禁用。
@@ -237,7 +237,8 @@ type WebShellSidebarSessionActionItem =
| 'export' // 📤 Export chat history (dropdown menu)
| 'delete' // 🗑 Delete session (dropdown menu)
| 'pin' // 📌 Pin/Unpin (inline button)
| 'archive'; // 📦 Archive (dropdown menu)
| 'archive' // 📦 Archive (dropdown menu)
| 'split'; // ◫ Add to Split View (dropdown menu)
/** Subset with working inline (hover-button) handlers. */
type WebShellSidebarSessionInlineActionItem =
@@ -255,7 +256,7 @@ interface WebShellSidebarSessionActionsOptions {
Controls which action buttons appear on session rows:
- **`items`**: Master control for all actions (both inline and dropdown). If an item is not in `items`, it's hidden everywhere.
- **`inlineItems`**: Controls which items appear as **inline buttons** (on hover) on standard session rows. Defaults to `['pin']`. Only items with working inline handlers can be used: `'pin'`, `'rename'`, `'export'`, `'delete'`. `'details'`, `'group'`, and `'archive'` are dropdown-only on standard rows.
- **`inlineItems`**: Controls which items appear as **inline buttons** (on hover) on standard session rows. Defaults to `['pin']`. Only items with working inline handlers can be used: `'pin'`, `'rename'`, `'export'`, `'delete'`. `'details'`, `'group'`, `'archive'`, and `'split'` are dropdown-only on standard rows.
Archived rows in Settings use Export conversation and Restore as inline buttons, with Rename and Delete in More. Their actions still follow `items` and built-in availability restrictions; `inlineItems` does not change this layout. The existing styles reveal these buttons on hover or keyboard focus and keep them visible on touch devices.
@@ -274,6 +275,12 @@ their capability condition and `items` include them. Archive is disabled on
the current session and on any session with a running turn, because the daemon
closes the live session when it archives.
`'split'` is default-enabled like the other items, but it only takes effect
when the host wires `onAddSessionToSplitView`. The sidebar also reads its
`splitSessionIds` prop for capacity: a session that is already open in the
split renders the item disabled with an explanatory title, as does the
pane-limit case and the current session while the split is closed.
```tsx
sidebar={{
sessionActions: {
+1 -1
View File
@@ -781,7 +781,7 @@ The following events are logged:
- `loop_protection`: A loop detector governed by `model.loopProtection` reached a reminder or stop point. _(Note: emitted without `qwen-code.` prefix, like `loop_detected`.)_
- **Attributes**: `prompt_id` (string), `loop_type` (string), `decision` (string: `reminded`, `stopped` or `observed`), `mode` (string: `stop`, `warn` or `off`), `loop_reason` (string, optional)
- `reminded` is logged when the reminder is sent to the model; `stopped` is logged once per turn and also as `loop_detected`; `observed` is logged once per pattern under `off`, and under `warn` when a stateful read tool reaches the stop point without the result evidence a reminder needs. Budget stops (an explicit cap, the backstop) and the duplicate provider tool-call id guard are not governed and log no `loop_protection` event.
- `reminded` is logged when the model accepts the request carrying the reminder — the first stream event of that request — not when the reminder is queued or prepared, so a send that never reaches the model stays un-credited and the same warning is delivered by a later send; `stopped` is logged once per turn and also as `loop_detected`; `observed` is logged once per pattern under `off`, and under `warn` when a stateful read tool reaches the stop point without the result evidence a reminder needs. Budget stops (an explicit cap, the backstop) and the duplicate provider tool-call id guard are not governed and log no `loop_protection` event.
- `kitty_sequence_overflow`: Kitty graphics protocol sequence exceeded buffer size. _(Note: emitted without `qwen-code.` prefix — pre-existing inconsistency.)_
- **Attributes**: `sequence_length` (int), `truncated_sequence` (string, first 20 chars)
+8 -3
View File
@@ -139,9 +139,14 @@ whole bearer-gated API, and a trusted loopback caller gets full authority
including code execution as the daemon user. There is no per-end-user principal
model. If you are putting
this behind a multi-user product, your backend owns user identity and must not
hand the daemon token to browsers. Containerised and multi-tenant deployment
are explicitly deferred — see "v0.16-alpha known limits" in the
[user guide](../users/qwen-serve.md).
hand the daemon token to browsers. Containerised deployments must apply the
same listener, token, and browser-origin rules. Multi-tenant deployments also
require an external orchestrator with separate daemons for independent
security principals — see
[Deployment and security notes](../users/qwen-serve.md#deployment-and-security-notes)
and
[Multi-session & multi-workspace deployment](../users/qwen-serve.md#multi-session--multi-workspace-deployment)
in the user guide.
Configured channel webhook ingress (`POST /channels/:channelName/webhooks/:source`)
uses its own `x-qwen-webhook-secret` authentication before bearer
@@ -0,0 +1,75 @@
# Channel Request Owner Approval Implementation Plan
**Goal:** Allow an admitted request owner to approve once or deny in direct and
group shared sessions while retaining operator-only management and delegation.
**Architecture:** Keep shared-session routing and operator predicates unchanged.
Record each permission's active run and filter non-operator lookup results by
that run's attended owner before exposing any request details.
**Tech stack:** TypeScript, existing ChannelBase relay, Vitest.
**Spec:** [Request owner approval](../design/channel-request-owner-approval.md).
## Constraints and Review Focus
- No new public configuration, dependency, route, or automatic migration.
- Explicit operators, including `[]`, still control shared-session management.
- Cross-sender, cross-chat, cross-thread, unattended and stale-run IDs must not
become owner approvals or leak request titles through ambiguity reporting.
- `/approve-always` does not gain the shared-session owner exception.
- Preserve named selection, operator delegation and native-card settlement.
## Task: Request-Scoped Owner Authority
Files: `packages/channels/base/src/ChannelBase.ts`, its collocated tests,
channel overview and synchronized English/Chinese design documents.
- [x] Add regressions exercising attended owners and denied delegation; run
`cd packages/channels/base && npx vitest run src/ChannelBase.test.ts -t 'request owner approval'`
and observe owner approvals fail against the existing operator-only gate.
- [x] Add the private run snapshot and owner filtering; keep all other shared
commands on the existing operator predicate.
- [x] Run the regression subset, then the complete ChannelBase test file.
- [x] Update the channel overview's request-vs-session permission contract.
- [x] Run changed-file formatting and lint checks, package compilation, and
independent compiled-code reproduction verification.
- [x] Complete whole-repository build, typecheck, and bundle after synchronizing
the installed dependencies and sibling build outputs with this checkout.
- [x] Read the full diff in two clean self-audit passes, then obtain a fresh
code review and fix correctness findings before reporting completion.
The user subsequently requested full verification, an issue following the
community template, and a linked PR before finishing the full checks. Track the
reported failure in [#16086](https://github.com/QwenLM/qwen-code/issues/16086).
Submission does not authorize a release, merge, or deployment.
## Verification Evidence
- The first 28 owner regressions failed against the original operator gate,
then passed after the fix. The complete updated file passed 783 tests.
- `npm run build` from `packages/channels/base` passed; changed-file ESLint,
Prettier and whitespace checks passed.
- Independent compiled ChannelBase inbound/event verification passed 33
scenarios, including a real isolated PairingStore. This is not live IM E2E.
- After refreshing pinned dependencies and synchronizing main, root
`npm run build`, `npm run bundle`, and
`NODE_OPTIONS=--max-old-space-size=6144 npm run typecheck` passed. The refreshed
ChannelBase test file passed all 783 tests again.
- Independent concurrent-session verification passed 13 scenarios, including
ordinary-member isolation and the existing operator delegation behavior.
- A fresh bundled CLI, isolated extension transport, real ACP child, local
OpenAI-compatible fixture, and real edit tool passed five end-to-end cases:
direct approval, group approval after another member's rejection, group
denial, persistent/management rejection followed by one-time approval, and
privately paired approval. Approved edits changed files; denial preserved
the file and completed the attended lifecycle without a second request.
- CI static checks, TUI snapshots, and no-AK integration passed on the tested
production head. Full Linux unit and Web Shell smoke jobs were still running
when this evidence was recorded. No live IM or all-platform acceptance is
claimed.
- Independent review found no actionable correctness or security defects. Its
suggested original-owner-versus-current-sender regression was added and
included in the final 783-test run. An active unattended loop/webhook case
remains outside the independent integration coverage; the owner filter and
existing automation authorization were inspected without broadening them.
+2 -2
View File
@@ -439,7 +439,7 @@ The bridge-availability and missing-bridge warning rules below describe direct t
| -------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `memory.enableManagedAutoMemory` | boolean | Enable background extraction of memories from conversations. | `true` |
| `memory.enableManagedAutoDream` | boolean | Enable automatic consolidation (deduplication and cleanup) of collected memories. | `true` |
| `memory.enableAutoSkill` | boolean | Enable background review for reusable project skills after tool-heavy sessions. | `true` |
| `memory.enableAutoSkill` | boolean | Enable background review for reusable project skills after tool-heavy sessions. | `false` |
| `memory.autoSkillConfirm` | boolean | Ask for confirmation before auto-generated skills are added to the skill library. When off, auto-skills are saved immediately. | `true` |
| `memory.enableTeamMemory` | boolean | Enable a project memory tier shared with collaborators via the git-tracked `.qwen/team-memory/` directory. Writes to it are secret-scanned and reviewable in the git diff. | `false` |
| `memory.enableTeamMemorySync` | boolean | When team memory is enabled, automatically commit, fast-forward-pull, and push the `.qwen/team-memory/` directory at session start so collaborators stay in sync. Requires a configured git upstream. | `false` |
@@ -648,7 +648,7 @@ LSP server configuration is done through `.lsp.json` files in your project root
| `security.folderTrust.enabled` | boolean | Setting to track whether Folder trust is enabled. | `false` |
| `security.auth.selectedType` | string | The currently selected authentication type. | `undefined` |
| `security.auth.enforcedType` | string | The required auth type (useful for enterprises). | `undefined` |
| `security.auth.useExternal` | boolean | Whether to use an external authentication flow. | `undefined` |
| `security.auth.useExternal` | boolean | Whether to use an external authentication flow. | `false` |
| `security.auth.apiKey` | string | **Deprecated.** API key for OpenAI-compatible authentication. Migrate to `modelProviders` with `envKey` instead — see [Model Providers](./model-providers). | `undefined` |
| `security.auth.baseUrl` | string | **Deprecated.** Base URL for the OpenAI-compatible API. Migrate to `modelProviders` instead — see [Model Providers](./model-providers). | `undefined` |
| `security.allowedInsecureVoiceBaseUrls` | array of strings | Complete voice provider base URLs that may use HTTP or resolve to private-network addresses. Each entry must include an explicit `http://` or `https://` scheme and the full path (for example, `/v1`); only URL serialization and trailing slashes are normalized. Wildcards are not supported; metadata, link-local, local-use NAT64, 6to4, and Teredo addresses remain blocked even when listed, as do hostnames that resolve to loopback; IPv4-mapped, IPv4-compatible, and well-known NAT64 (`64:ff9b::/96`) literals are classified by their embedded IPv4 address. Only User, System, and SystemDefaults scopes are honored. Use only for trusted endpoints in managed private networks. Cleartext HTTP also exposes the provider API key transmitted in the Authorization header. An allowlisted hostname is only as trustworthy as its DNS; prefer IP-literal entries when the gateway address is stable. The exact match covers the batch request URL; streaming transports connect to a WebSocket URL derived from it (same scheme, host, and port, `/api-ws/v1/...` path), not to the allowlisted path itself. | `[]` |
+90 -11
View File
@@ -137,6 +137,7 @@ this recovery exception.
| API events | `command.register`, `ui.log`, `ui.invalidate` and the session and clock methods pass through the middleware chain with host-supplied origin |
| Session | `$.plugin`, read-only `$.session` facts, `$.clock.now` and `$.clock.sleep`, described in [Session facts and clock](#session-facts-and-clock) |
| Turns | `turn.start`, `turn.complete` and `$.turn.abort` for the prompts a person submits, described in [Turn events](#turn-events) |
| Prompts | `prompt.submit` in headless runs, described in [Submitted prompts](#submitted-prompts) |
| Logging | `$.ui.log` returns void and queues delivery; transcript logs use stderr in text mode or `system/ui_log` in JSON modes; debug logs stay in the debug logger; neither becomes model input |
| Elements | `$.ui.resolve`, `$.ui.invalidate` and the `h` and `Fragment` globals, described in [Elements](#elements) |
| Drawing | `ui.render` for the band above the prompt in the Ink and OpenTUI terminals, described in [The band above the prompt](#the-band-above-the-prompt) |
@@ -157,18 +158,18 @@ command names are matched against the resulting name.
## Middleware and recovery
Patterns select among the engine events `session.start`, `session.end`,
`command.run`, `tool.call`, `tool.check`, `turn.start`, `turn.complete` and, in the Ink and OpenTUI terminals, `ui.render`, and the API event of each supported
`command.run`, `tool.call`, `tool.check`, `turn.start`, `turn.complete`, in headless runs `prompt.submit`, in the Ink and OpenTUI terminals `ui.render`, and the API event of each supported
`$` method, which `$.session.version()` lists. Use an exact name, a prefix such as `command.*`,
`*`, or a single negation such as `!session.*`. The patterns `!*` and `!!...` are
rejected.
Claude Code 2.1.310 also defines events that Qwen Code does not run yet, such as
`prompt.submit` or `classic.Stop`, and `ui.render` runs only where a host draws
it. A registration for one of them,
Claude Code 2.1.316 also defines events that Qwen Code does not run yet, such as
`prompt.fill` or `classic.Stop`. `prompt.submit` runs only in headless runs, and
`ui.render` only where a host draws it. A registration for one of them,
or for a prefix such as `prompt.*` that matches none of the events above, loads
but never runs, so the rest of the Mod keeps working. At startup each plugin
reports its skipped patterns once, where transcript logs appear:
`Hooks for unsupported events will not run: prompt.submit, ui.render`. A skipped
`Hooks for unsupported events will not run: prompt.fill, ui.render`. A skipped
hook never runs, including a guard and its `.catch`. Names that Claude Code does
not define, such as `tool.calls`, still reject initialization.
@@ -200,8 +201,9 @@ registrations with matchers, including equal matchers. Each wildcard or negated
pattern can be registered once. Within a plugin, registration order determines
the chain; consumers run before their declared dependency providers. A handler
can answer early, including at `session.start`, preventing later handlers from
running. Only command `args` can be rewritten on `command.run`; lifecycle and
command identity fields stay pinned.
running. Only command `args` can be rewritten on `command.run`, and only a
prompt's `text` and `context` on `prompt.submit`; lifecycle and command identity
fields stay pinned.
## Tool interception
@@ -414,7 +416,7 @@ supports:
{
"version": "0.26.0",
"qwen": {
"claudeCode": "2.1.310",
"claudeCode": "2.1.316",
"events": [
"clock.now",
"clock.sleep",
@@ -511,7 +513,7 @@ Mod discovery. Mod manifests must be relative paths inside the extension.
directory, the one that holds `qwen-extension.json` or
`.claude-plugin/plugin.json`. They are plain properties.
The other methods are API events that resolve to the shapes Claude Code 2.1.310
The other methods are API events that resolve to the shapes Claude Code 2.1.316
declares:
| Method | Resolves to |
@@ -625,6 +627,83 @@ ending. In a headless run the turn ends with the error result
its next message. Qwen Code has no `$.prompt.submit` yet, so a Mod cannot send a
prompt after it ends a turn.
## Submitted prompts
In headless runs, `prompt.submit` fires for each prompt a person submits, before
UserPromptSubmit hooks and before `@` references are read. A hook can rewrite the
prompt, attach context the model reads beside it, or drop it:
```javascript
export function register(on) {
on('prompt.submit', ($, e, next) => {
if (/AKIA[0-9A-Z]{16}/.test(e.text))
return { drop: 'the prompt holds an AWS access key' };
return next({
...e,
text: e.text.replace(/\bprod\b/g, 'production'),
context: [...(e.context ?? []), 'Answer in British English.'],
});
});
}
```
The hook receives:
| Field | Value |
| --------- | ------------------------------------------------------------------------------------- |
| `text` | the prompt as submitted; for a stream-json message, its text blocks |
| `context` | absent; each hook that attaches context passes the list on with its own entries added |
| `wait` | `false` |
| `origin` | `{kind: 'sdk'}` |
`origin`, `wait` and `turnId` are pinned. The prompt that enters is the one that
arrives beneath the hooks: `next(e)` resolves to `{text, context?, origin}`, and
what a hook answers after its `next` resolved changes nothing. The rewritten text
is what the model reads, what `turn.start` hears and what the transcript keeps.
In a stream-json message with blocks other than text, the rewrite replaces its
text blocks and the other blocks follow it.
The context goes to the model after the prompt, in the block that holds
UserPromptSubmit's context, ahead of it, and is never shown as part of the
prompt.
`{drop: reason}` stops the prompt before UserPromptSubmit and before any model
request. The run ends with the error result `Prompt dropped by a hook: <reason>`
and exit code 1; a stream-json session goes on to its next message. The reason
is one line, kept to its first 4,096 characters. A hook that answers without
calling `next` and without a drop also stops the prompt, with the result
`Prompt not submitted: a hook answered without passing the prompt on`. A hook
that calls `next` and then answers a drop fails, and its `next` stands.
A hook that fails or times out is skipped, so a broken guard never blocks a
prompt. Give a guard a `.catch` that drops instead:
```javascript
on('prompt.submit', guard).catch(($, e, next) =>
next.called ? next(e) : { drop: 'the guard failed' },
);
```
A prompt longer than 128 KiB as JSON reaches no hook, and the line
`prompt.submit hook skipped: the prompt is too long for a Mod payload` appears
where transcript logs do.
These differ from Claude Code:
- `prompt.submit` runs only in headless runs; the Ink and OpenTUI terminals skip
its hooks at startup.
- Slash commands, background notifications and the continuation of an
interrupted turn raise no `prompt.submit`, so `command` and `isMeta` never
appear.
- `attachments` never appears, and `$.prompt.media` does not exist.
- A UserPromptSubmit hook that blocks the prompt beneath does not turn `next(e)`
into `{drop}`: the Mod hooks have settled by the time it runs.
- `next(e)` resolves once the hooks beneath have answered, not once the turn
starts.
- A context the conversation already holds word for word is not sent again.
Each Mod context and UserPromptSubmit's context count on their own, so a
context that changes every turn does not make the others repeat.
- `$.prompt.submit` does not exist yet.
## Elements
`$.ui.resolve(e)` returns the element table of the terminal: a frozen object
@@ -643,7 +722,7 @@ its props and its children flattened into an array, and throws for a string tag
JSX in a `.tsx` or `.jsx` file compiles to these calls, as described in
[TypeScript, JSX and relative imports](#typescript-jsx-and-relative-imports).
A tree a hook returns is judged with Claude Code 2.1.310's rules for `Box` and
A tree a hook returns is judged with Claude Code 2.1.316's rules for `Box` and
`Text`, and any other element refuses it with
`<Element> is not drawn by Qwen Code yet`.
@@ -890,6 +969,6 @@ Own `__proto__` keys in tool inputs and results are ordinary data on both sides.
A hook answer whose getter throws while Qwen Code reads it fails as a whole;
Claude Code 2.1.299 drops only that key.
The author API follows the Claude Code 2.1.310 declarations, event names
The author API follows the Claude Code 2.1.316 declarations, event names
included, and the
[official Mod documentation](https://code.claude.com/docs/en/plugins/mods/create).
+4 -2
View File
@@ -123,11 +123,13 @@ New managed channels default to `pairing`. The deprecated `senderPolicy` and `dm
### Shared-Session Operators
In a shared session (`sessionScope: "chat_thread"` or `"single"`, and group chats under `"thread"`), some commands affect everyone in the conversation: `/approve`, `/deny`, `/cancel`, `/clear`, `/who`, `/status`, `/loop`, `/btw`, the loop tool, and steering an in-flight turn. Only the session's operators may use them; other members' messages queue instead of steering. Sessions that are not shared belong to their own sender, who may always use them.
In a shared session (`sessionScope: "chat_thread"` or `"single"`, and group chats under `"thread"`), session management requires operator privileges: `/approve-always`, `/cancel`, `/clear`, `/who`, `/status`, `/loop`, `/btw`, the loop tool, and steering an in-flight turn. Other members' messages queue instead of steering. Sessions that are not shared belong to their own sender, who may always use them.
An admitted sender may use `/approve` (allow once) or `/deny` for their own pending tool request in their current attended turn, in direct messages and groups. This request-specific authority does not depend on `operators`, even when the list is empty. Another ordinary member cannot answer it, and an attempt from another chat or thread is rejected. Operators may still answer another sender's text permission request in the same chat and thread; native interactive cards retain their existing owner checks.
Set `operators` to the user IDs allowed to manage shared sessions. Omitted or empty `operators` grants nobody this role. Private allowlists, user pairing, group pairing, and group member lists never imply operator permissions. Operators must still pass normal conversation admission.
For example, `groups: { "*": { "senders": "open" } }` with `operators: ["alice"]` lets every admitted group member start a turn, but only Alice can manage its shared session.
For example, `groups: { "*": { "senders": "open" } }` with `operators: ["alice"]` lets every admitted group member start a turn and approve once or deny their own current request. Only Alice can manage its shared session or answer another sender's text permission request.
A saved loop is checked against the same rule when it fires, using its creator as the sender, and is disabled if the creator is no longer an operator.
+3 -1
View File
@@ -248,6 +248,8 @@ It applies to **PR targets only** (a local review's diff comes from a live worki
Two things to know. With only the built-in target default, a continuation keeps the interrupted run's recorded **effort**. An explicit `--effort`, a project-remembered level, the operator `review.effort` setting, or an effective `--comment` supplies a required level; if it differs from the interrupted run, resume is refused and a fresh run starts at that level, because different effort is different work. And if the PR head moved while the review was down, the resume refuses (`head-moved`) and the fresh run reviews the new commits — which is what you want, and it counts as this review's one restart.
`--resume` is **not** the flag for a re-review after the PR gained commits: just run `/review 123` again. At high effort a plain re-review reads the previous round's anchor and scopes itself to the new commits (plus the files that import them); lower efforts review the full diff. At high and medium effort it also rules on every finding the previous round posted; a `low` pass reads no ledger, so it neither re-rules nor carries those findings. After a completed round there is nothing to resume — cleanup removes the run's state, so `--resume` refuses as `no-report` and costs nothing. `worktree-gone` is a different shape: the attempt's report survived but its worktree vanished — nothing to continue, and no restart charged either. The `head-moved` refusal (and its one-restart cost) applies only when an interrupted attempt's state survived.
## Findings as Data
Confirmed findings are canonicalized into `.qwen/tmp/qwen-review-<target>-findings.json` before anything else consumes them — the terminal report, the saved Markdown report, and the PR review JSON all read that one artifact instead of re-typing the list. Each finding carries a unique `id` (what outcomes and resolved anchors join on), `severity`, `confidence`, `source`, `summary`, a `shortSummary` capped at 60 characters for list rendering, `failureScenario`, and one or more `locations` — a pattern-aggregated finding keeps **one location per occurrence**, so each still gets its own inline comment.
@@ -420,7 +422,7 @@ The exit code is the contract a gate should read:
`3` (not `2`) lets a gate distinguish "the review is blocking" from "the tool broke" — yargs already uses `1` for usage errors — without parsing any output. `--timeout-minutes` (default 120, floored at 1) terminates a hung review and exits `1`, and cancelling the command (Ctrl+C / SIGTERM) terminates the review's process group rather than orphaning it.
`--resume` continues an interrupted review of the same PR instead of starting over — when a long local run dies part-way (a dropped connection, a timeout, a killed terminal), the retry would otherwise re-fetch, re-chunk and re-launch agents whose work is already on disk. It is safe to pass unconditionally on a retry: `fetch-pr` rules on the on-disk state itself (worktree still at the fetched SHA and clean, diff bytes unchanged, PR head unmoved, resume cap unspent) and silently falls back to a fresh review whenever anything no longer matches, so the flag never fails a run that could start over. When the current invocation has only the built-in target default, a continuation stays pinned to the interrupted run's recorded effort. An explicit `--effort`, a project-remembered level, the operator `review.effort` setting, or an effective `--comment` supplies a required level; a mismatch refuses the resume and runs fresh at that level. PR targets only (a local review's diff is captured from a live working tree, which has no stable interrupted state to continue). Resume is a **local convenience**: the repository's own CI review workflow does **not** resume — each retry re-runs fresh, because a CI attempt runs no-sandbox and its worktree is deleted on exit, leaving no interrupted state to continue.
`--resume` continues an interrupted review of the same PR instead of starting over — when a long local run dies part-way (a dropped connection, a timeout, a killed terminal), the retry would otherwise re-fetch, re-chunk and re-launch agents whose work is already on disk. It is safe to pass unconditionally on a retry: `fetch-pr` rules on the on-disk state itself (worktree still at the fetched SHA and clean, diff bytes unchanged, PR head unmoved, resume cap unspent) and silently falls back to a fresh review whenever anything no longer matches, so the flag never fails a run that could start over. When the current invocation has only the built-in target default, a continuation stays pinned to the interrupted run's recorded effort. An explicit `--effort`, a project-remembered level, the operator `review.effort` setting, or an effective `--comment` supplies a required level; a mismatch refuses the resume and runs fresh at that level. PR targets only (a local review's diff is captured from a live working tree, which has no stable interrupted state to continue). Resume is a **local convenience**: the repository's own CI review workflow does **not** resume — each retry re-runs fresh, because a CI attempt runs no-sandbox and its worktree is deleted on exit, leaving no interrupted state to continue. It is also not the flag for a re-review after the PR gained commits — see **Resuming an interrupted review** above for what a plain re-review does at each effort and which refusal each surviving state produces.
A time-budgeted run can also export a **soft** deadline so the review stops its open-ended reverse-audit loop while there is still time to verify, compose and post: `QWEN_REVIEW_DEADLINE_EPOCH` is the Unix-seconds moment the run will be killed, and `QWEN_REVIEW_DEADLINE_RESERVE_SECONDS` (default 4800; `0` keeps only the round estimate) is the tail that must remain for the last round's verification, `compose-review` and submission. When the remaining budget no longer fits another round plus that tail, the round builder refuses to build it, and the composed verdict discloses the truncated audit (an otherwise-Approve verdict is capped at Comment). Without that variable the review uses the wall its plan recorded at capture: `--deadline <minutes>` on `qwen review fetch-pr` / `capture-local` / `plan-diff`, or by default **8 hours** on a small (Step 3A) diff, **12** on a large (3B) one and **16** on a huge one — sized by the diff's own lines, so a critical-posture fix-audit round keeps the wall its delta's size earns even though it fans out per territory and takes the chunked round cap. That default is a liveness bound — sized above what a healthy run at the round cap spends — so it ends a loop that has stopped converging and leaves a run that keeps working untouched; it is charged in elapsed time, idle included, so a session left overnight spends it, and because the next round is priced from the previous round's admission, a multi-hour pause between rounds can stop a run with time still on the wall (a `--resume` from a new session restarts both the wall and the pricing; **Review Deadline** below gives the ceilings). It stops the loop at the next round admission or verify build; it does not interrupt a build already running. A plan-recorded wall keeps a third of itself (capped at 4800 seconds, floored at the compose floor) as the tail reserve unless the reserve variable is set; a `--deadline` that cannot hold a convergence (two rounds plus that reserve — at or under ninety minutes, or more when the shell's reserve or compose-floor overrides raise the bar; that second leg is skipped while the environment exports a well-formed epoch, since the flag is inert then, and the wall it records is priced again in whatever shell a later env-less `--resume` runs in) is refused up front, and a wall just above it can still stop before round 1 once the fan-out and verification have spent it. When the wall can no longer admit a rebuild the coverage check would otherwise prescribe (a reverse-audit round, or a verifier shard under the compose floor), `compose-review` withholds that `FIX:` line and prints a `NOTE:` line saying so, with the gate's arithmetic; the gap is still disclosed in the posted body. The plan wall bounds the round builder and the verifier; two readers stay on the environment alone — the generated review workflow's launch allowance, and `qwen review run`'s own `--timeout-minutes` kill, which is not exported as a deadline. The plan stores the wall as a duration from the attempt's start, so a `--resume` from a new session renews it (a same-session resume continues the attempt and its wall; the flag itself is ignored on a resumed run — only its grammar and the default rule are checked — and the run says what wall the plan holds and how much of it is left, or that it has run out, and which builds the gates will now refuse; a wall can only run out on a same-session resume, or when the ledger failed to record the attempt, so the cheapest way past it is to resume from a new session, which renews the wall and keeps the worktree; a fresh review without `--resume` records a new wall too, and `--deadline none` on that fresh capture records none). `--deadline none` records no wall and leaves the loop bounded by the round cap alone — unless the environment exports an epoch, which clocks the run regardless. The `/review` slash command does not take the flag: it warns and drops it together with a value that could be one (whole minutes, or `none` — so a PR number typed right after it is dropped too), while any other value after it is disposed of like an invalid `--effort` value (a PR URL becomes the target; a typo is reported, and survives only as a sole file-shaped token), and a skill-driven run always uses the default. The huge tier's round reduction (3 rounds instead of 5) keys on an explicit deadline only — the environment's or the flag's — never on the default. A malformed environment deadline falls through to the plan's wall; a malformed plan field leaves the review ungated — the outer timeout still bounds the run.
@@ -246,7 +246,8 @@ The embedded Runtime Broker: its private HTTP listener for the Hosted Harness, t
| `operator-recovery-enabled` | `QWEN_MANAGED_AGENT_RUNTIME_OPERATOR_RECOVERY_ENABLED` | boolean | `false` | Whether the offline operator-recovery command (`bin/qwen-managed-agent-operator-recovery`) may run its `inspect`, `prepare` and `complete` steps for a Hosted Shell capture that kept a Workspace lease. Set it for that maintenance process: the command refuses to run unless it is on with `qwen.managed-agent.runtime-broker.durable-local-process`, the `local-process` provisioner and `qwen.managed-agent.runtime-broker.state-directory`. The server refuses it at startup with an external provisioner. |
| `port` | `QWEN_MANAGED_AGENT_RUNTIME_BROKER_PORT` | integer | `4182` | The TCP port of the embedded Runtime Broker's HTTP listener; the Hosted Harness's `QWEN_MANAGED_RUNTIME_BROKER_URL` must reach it, directly on a loopback address or through the TLS-terminating proxy in front of a non-loopback bind. Startup fails when the listener cannot bind it. Preflight checks that the port is free, or listening with `--online`, and compares it with the Broker URL in the Harness credentials file. |
| `previous-credential-keys` | — | YAML map | *(empty)* | Earlier Runtime credential keys kept after a rotation, as a map from key ID to base64-encoded 32-byte key, so rows encrypted under them stay readable; writes use only the active key. Secrets. Set it in a configuration file (YAML), each ID in brackets, such as `'[old-v1]': <key>`; environment variables cannot supply it. Startup refuses a key that is not 32 bytes or an ID equal to `qwen.managed-agent.runtime-broker.credential-key-id`. |
| `provisioner` | `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER` | string | `local-process` | Which provisioner places Runtime workers: `local-process` (Node.js workers on this host), `static` (one preconfigured worker at `qwen.managed-agent.runtime-broker.static-endpoint`), `external-scratch` or `external-workspace` (an operator-run HTTP provider at `qwen.managed-agent.runtime-broker.external-provider-url`), or `kubernetes-workspace` (resident Workspaces from `qwen.managed-agent.runtime-broker.resident-workspaces`). Startup refuses any other value, including `kubernetes`. |
| `provider-options` | — | YAML map | `{}` | Provider-specific string settings passed to the selected Runtime factory, configured as a YAML map. Their meaning and validation belong to that provider; built-in backends continue to use their existing settings. Defaults to an empty map. |
| `provisioner` | `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER` | string | `local-process` | The installed factory kind that places Runtime workers: `local-process` (local Node.js workers), `ssh-process` (registered SSH Workspaces), `static` (a preconfigured worker), `external-scratch` or `external-workspace` (an operator-run HTTP provider), or `kubernetes-workspace` (resident CSI Workspaces). A trusted additional provider may register its own kind. With the Broker enabled, missing or duplicate kinds fail before resources are created; `kubernetes` remains reserved and rejected. Embedding hosts must explicitly include the selected backend artifact. |
| `resident-kubernetes-api-url` | — | string | *(empty)* | The HTTPS origin of the Kubernetes API server that the `kubernetes-workspace` provisioner calls for `qwen.managed-agent.runtime-broker.resident-workspaces`, together with `qwen.managed-agent.runtime-broker.resident-kubernetes-token-file` and `qwen.managed-agent.runtime-broker.resident-kubernetes-ca-file`. Startup refuses resident Workspaces without it, and refuses a URL that is not `https` or has a path, credentials, query or fragment. The resident-retirement maintenance command reads it too. |
| `resident-kubernetes-ca-file` | — | string | *(empty)* | Path to a PEM or DER file of X.509 CA certificates that the server trusts, instead of the JVM defaults, for TLS to the Kubernetes API named by `qwen.managed-agent.runtime-broker.resident-kubernetes-api-url`. Required when `qwen.managed-agent.runtime-broker.resident-workspaces` is set; an unreadable file or one without certificates fails startup. The `resident-retirement` admin command reads it too. |
| `resident-kubernetes-token-file` | — | string | *(empty)* | Path to a file holding the bearer token the server sends to the Kubernetes API to manage resident Workspace Pods. The file holds a secret; it is re-read on every request, so the token can be rotated in place, and it must contain at most 16 KiB of letters, digits and `._~-`. Required when `qwen.managed-agent.runtime-broker.resident-workspaces` is set; the `resident-retirement` admin command reads it too. |
@@ -246,7 +246,8 @@ Workspace 扩展包的发布与管理,以及哪些 actor 有权执行这些操
| `operator-recovery-enabled` | `QWEN_MANAGED_AGENT_RUNTIME_OPERATOR_RECOVERY_ENABLED` | 布尔 | `false` | 是否允许离线的运维恢复命令(`bin/qwen-managed-agent-operator-recovery`)对仍占用 Workspace 租约的 Hosted Shell 捕获执行 `inspect`、`prepare` 与 `complete` 步骤。请在该维护进程中设置:除非此项开启,且同时开启 `qwen.managed-agent.runtime-broker.durable-local-process`、使用 `local-process` provisioner 并配置 `qwen.managed-agent.runtime-broker.state-directory`,否则该命令拒绝运行。使用外部 provisioner 时,服务端启动时拒绝此项。 |
| `port` | `QWEN_MANAGED_AGENT_RUNTIME_BROKER_PORT` | 整数 | `4182` | 内嵌 Runtime Broker HTTP 监听器的 TCP 端口;Hosted Harness 的 `QWEN_MANAGED_RUNTIME_BROKER_URL` 必须能到达它:回环地址时直接指向它,非回环绑定时经由前置的 TLS 终止代理。监听器无法绑定该端口时启动失败。预检会检查端口是否空闲(使用 `--online` 时检查是否在监听),并与 Harness 凭据文件中的 Broker URL 比对。 |
| `previous-credential-keys` | — | YAML 映射 | *(空)* | 密钥轮换后保留的旧 Runtime 凭据密钥,形式为 key ID 到 base64 编码 32 字节密钥的映射,使以旧密钥加密的行仍可读取,而写入始终只使用当前密钥。属于机密。须在配置文件(YAML)中设置,并用方括号包住每个 ID,例如 `'[old-v1]': <key>`;无法通过环境变量提供。密钥不是 32 字节,或 ID 与 `qwen.managed-agent.runtime-broker.credential-key-id` 相同时启动时拒绝。 |
| `provisioner` | `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER` | 字符串 | `local-process` | 放置 Runtime worker 所用的 provisioner:`local-process`(本机上的 Node.js worker)、`static`(位于 `qwen.managed-agent.runtime-broker.static-endpoint` 的单个预配置 worker)、`external-scratch` 或 `external-workspace`(位于 `qwen.managed-agent.runtime-broker.external-provider-url` 的运维方 HTTP provider),或 `kubernetes-workspace`(来自 `qwen.managed-agent.runtime-broker.resident-workspaces` 的常驻 Workspace)。其他取值(包括 `kubernetes`)启动时拒绝。 |
| `provider-options` | — | YAML 映射 | `{}` | 传递给选中 Runtime 工厂的专用字符串配置,以 YAML 映射设置。其含义及校验由该 provider 决定;内置后端继续使用原有配置项。默认为空映射。 |
| `provisioner` | `QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER` | 字符串 | `local-process` | 放置 Runtime worker 所用的已安装工厂 kind:`local-process`(本机 Node.js worker)、`ssh-process`(已注册 SSH Workspace)、`static`(预配置 worker)、`external-scratch` 或 `external-workspace`(运维方 HTTP provider),或 `kubernetes-workspace`(常驻 CSI Workspace)。其他可信 provider 可以注册自己的 kind。Broker 启用时,缺失或重复的 kind 在创建资源前拒绝;`kubernetes` 仍为保留值且被拒绝。嵌入服务须显式添加所选后端产物。 |
| `resident-kubernetes-api-url` | — | 字符串 | *(空)* | `kubernetes-workspace` provisioner 为 `qwen.managed-agent.runtime-broker.resident-workspaces` 调用的 Kubernetes API server 的 HTTPS origin,需与 `qwen.managed-agent.runtime-broker.resident-kubernetes-token-file`、`qwen.managed-agent.runtime-broker.resident-kubernetes-ca-file` 一起配置。配置了常驻 Workspace 却未设置此项,或 URL 不是 `https`、带有路径、凭据、查询串或片段时,启动时拒绝。常驻 Workspace 回收维护命令也会读取此项。 |
| `resident-kubernetes-ca-file` | — | 字符串 | *(空)* | 服务端连接 `qwen.managed-agent.runtime-broker.resident-kubernetes-api-url` 所指的 Kubernetes API 时用于校验 TLS 的 X.509 CA 证书文件路径(PEM 或 DER),只信任其中的证书,不使用 JVM 默认信任库。设置了 `qwen.managed-agent.runtime-broker.resident-workspaces` 时必填;文件不可读或不含证书时启动失败。管理命令 `resident-retirement` 也会读取它。 |
| `resident-kubernetes-token-file` | — | 字符串 | *(空)* | 保存 Bearer 令牌的文件路径,服务端用该令牌调用 Kubernetes API 管理常驻 Workspace 的 Pod。文件内容是密钥;每次请求都会重新读取,因此可以原地轮换,内容最多 16 KiB,只能包含字母、数字和 `._~-`。设置了 `qwen.managed-agent.runtime-broker.resident-workspaces` 时必填;管理命令 `resident-retirement` 也会读取它。 |
@@ -200,7 +200,7 @@ The [Helm chart](https://github.com/QwenLM/qwen-code/tree/main/deploy/kubernetes
### Maintenance commands
`bin/qwen-managed-agent-admin <command> [arguments]` runs the offline maintenance entry points, such as `storage-registration`, `workspace-migration` and the CSI commands, from the server jar through Spring Boot's `PropertiesLauncher`. Run it without arguments to list the commands, and run a command without arguments for its usage. Like the server launcher, it uses `config/application.yml` when that file exists and `SPRING_CONFIG_ADDITIONAL_LOCATION` is not set. Most commands that open the database take their own credentials rather than `server.env`'s `SPRING_DATASOURCE_*`: the Workspace commands read `W1_JDBC_URL`, `W1_JDBC_USER` and `W1_JDBC_PASSWORD`, and the CSI commands read `K2_JDBC_URL`, `K2_JDBC_USER` and `K2_JDBC_PASSWORD`. Two are exceptions: `operator-recovery` and `resident-retirement` start the server's Spring configuration and use its datasource, so they read `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME` and `SPRING_DATASOURCE_PASSWORD`, together with the other control-plane settings, from the environment and `config/application.yml`; run them with `server.env` loaded. `csi-protection` opens no database, and neither does `resolved-configuration`, which prints the Runtime state directory, auth mode and trusted actor header that the server's configuration resolves to in the environment it runs in; the upgrade command runs it the same way to find the Runtime state. Each command is an offline procedure with its own preconditions, documented with it; meet them before running it. `bin/qwen-managed-agent-operator-recovery` remains the entry for the Hosted Shell recovery procedure, and `bin/qwen-managed-agent-admin operator-recovery` runs the same command.
`bin/qwen-managed-agent-admin <command> [arguments]` runs the offline maintenance entry points, such as `storage-registration`, `workspace-migration` and the CSI commands, from the server jar through Spring Boot's `PropertiesLauncher`. Run it without arguments to list the commands, and run a command without arguments for its usage. Like the server launcher, it uses `config/application.yml` when that file exists and `SPRING_CONFIG_ADDITIONAL_LOCATION` is not set. Most commands that open the database take their own credentials rather than `server.env`'s `SPRING_DATASOURCE_*`: the Workspace commands read `W1_JDBC_URL`, `W1_JDBC_USER` and `W1_JDBC_PASSWORD`, and the CSI commands read `K2_JDBC_URL`, `K2_JDBC_USER` and `K2_JDBC_PASSWORD`. Two are exceptions: `operator-recovery` and `resident-retirement` start the server's Spring configuration and use its datasource, so they read `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME` and `SPRING_DATASOURCE_PASSWORD`, together with the other control-plane settings, from the environment and `config/application.yml`; run them with `server.env` loaded. `csi-protection` opens no database, and neither does `resolved-configuration`, which prints the Runtime state directory, auth mode and trusted actor header that the server's configuration resolves to in the environment it runs in, and the URLs of the control plane, its health endpoint and the Hosted Harness; the upgrade command runs it the same way to find the Runtime state and those URLs. Each command is an offline procedure with its own preconditions, documented with it; meet them before running it. `bin/qwen-managed-agent-operator-recovery` remains the entry for the Hosted Shell recovery procedure, and `bin/qwen-managed-agent-admin operator-recovery` runs the same command.
### Upgrade
@@ -228,6 +228,7 @@ Each phase reads the units, their environment files and the installed release ag
- `check` and `backup` take the Runtime state directory from the server's own configuration. They run the new release's server jar as the control-plane unit runs the server: with its account, working directory and environment, and the launcher's default `config/application.yml` when `SPRING_CONFIG_ADDITIONAL_LOCATION` is unset or empty. The jar prepares Spring's environment the way the server does, so every configuration file, import and profile counts, as do `SPRING_APPLICATION_JSON`, system properties from `JAVA_TOOL_OPTIONS` and relaxed names, and a relative directory is resolved in the unit's working directory. It starts nothing and opens no connection. When it cannot resolve the configuration, or the unit passes the launcher arguments, which `systemctl` does not print exactly, `backup` refuses until `--runtime-state <directory>` names the directory the server uses, or `--runtime-state none` says it keeps none. Without a configured directory, as on a deployment without the tool path, `backup` archives no Runtime state and says so.
- `install --backup <directory>` refuses a backup of another release, a backup file that changed, a backup the control plane has started since, and a backup of another database than the unit's `SPRING_DATASOURCE_URL` names. It copies the release to `<distribution>-releases/<version>`, such as `/opt/qwen-managed-agent-releases/<version>` (`--releases` names another directory; pass the same one to every phase), copies operator files from the installed `config/` with their owner and mode, and points the distribution path at the new release with one rename. The copy's own files, those `SHA256SUMS` lists, are made readable by every account, so the service account can run a release extracted with a restrictive umask. The first run moves the installed directory to `<releases>/<old version>` and puts a link in its place, so the units keep their paths. The previous release stays where it is. An existing `<releases>/<version>`, such as the archive extracted there, is used only when its `SHA256SUMS` is the new distribution's and its files match it, and its listed files are made readable the same way; a copy that fails is removed. A crash between the first run's two renames leaves `<distribution>.upgrade-<suffix>` beside the missing distribution path; every phase then names the `mv -T` that finishes the install. An install killed at another step can leave a partial copy `.<version>.partial-<suffix>` in the releases directory, an `<distribution>.upgrade-<suffix>` link beside the distribution path, or an operator file's `.tmp-<suffix>` copy in the new release's `config/`; the `layout` row lists them, and the next `install` removes them. The suffix is the eight hexadecimal digits `install` generates, so an operator's own `<distribution>.upgrade-backup` is left alone. Run again once the release is installed, as after an interrupted session, `install` says so and points at `start`.
- `start` refuses before `install`, and while the Harness Broker settings still need step 5 below. It starts the Harness, or restarts one that an earlier `start` left running so that it reads what was fixed since, runs `bin/qwen-managed-agent-server --check` as the service account with the unit's environment, starts the control plane only when that passes, and waits for a healthy actuator (`--timeout`, 300 seconds by default; one probe waits at most 5 seconds). Flyway migrates the schema during this start. A control plane that exits before it is healthy is stopped and reported, even one that systemd's `Restart=on-failure` would bring up when it runs it again: an exit during the start that migrates the schema is worth reading in the journal before `start` runs again. One that is still not healthy at the timeout keeps running; read its journal, then run `verify`, or `stop` and `start` again.
- `start` and `verify` find the Harness, the control plane and its health endpoint the same way, so a port or address set where `server.env` does not show it counts: in a configuration file, on the command line or in `SPRING_APPLICATION_JSON`. The Harness is where `qwen.managed-agent.harness.base-url` says. The control plane is where `server.port`, `server.address` and `server.ssl` bind it, with a wildcard address reached on the loopback interface, and the health endpoint follows Spring Boot's management settings: a `management.server.port` of its own moves it to that port, on `management.server.address` and under `management.server.base-path`. `--server-url <url>` names the control plane instead, for example behind a proxy, and the health endpoint is then `<url>/actuator/health`. A port of 0 or below gives no URL to wait on, so both phases refuse until `--server-url` names one; they also refuse a listener no URL can name, such as a link-local IPv6 address with its zone. When the configuration does not resolve, or the unit passes the launcher arguments, they use the URLs `server.env` names and print a line that says so.
- `verify` runs `--check --online` the same way and reads the actuator, waiting up to `--timeout` (300 seconds by default) for its answer. With `--tenant <id>`, it reads the tenant's most recent Session and its Items, before `--turn` creates one; `--turn` creates a Session and waits for one Turn, which calls the model, polling past a `5xx` answer or a dropped connection; when the Session read is skipped, the `turn` row says so. No request outlasts `--timeout`. A request without an actor lists only Sessions outside Workspaces; `--actor <id>` sends one through the header `QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER` names. `verify` reads the auth mode the way `backup` reads the Runtime state directory, and signed authentication skips both reads; read a Session through the ingress instead.
The command starts and stops units only. It never stops ingress, installs Qwen Code, edits environment or unit files, or deletes a release. When `check` warns that a unit template changed, carry the change over to the installed unit and run `systemctl daemon-reload` before `start`.
@@ -200,7 +200,7 @@ Hosted Harness 通过它自己的环境访问同一个 Broker,既不经命令
### 维护命令
`bin/qwen-managed-agent-admin <command> [arguments]` 通过 Spring Boot 的 `PropertiesLauncher`,从 server jar 运行各个离线维护入口,例如 `storage-registration`、`workspace-migration` 和各条 CSI 命令。不带参数运行可列出所有命令;某条命令不带参数运行会打印它的用法。与 server 启动脚本一样,当 `config/application.yml` 存在且未设置 `SPRING_CONFIG_ADDITIONAL_LOCATION` 时,它会使用该文件。大多数需要连接数据库的命令使用各自的凭据,而不是 `server.env` 中的 `SPRING_DATASOURCE_*`:Workspace 相关命令读取 `W1_JDBC_URL`、`W1_JDBC_USER` 和 `W1_JDBC_PASSWORD`,CSI 相关命令读取 `K2_JDBC_URL`、`K2_JDBC_USER` 和 `K2_JDBC_PASSWORD`。有两个例外:`operator-recovery` 和 `resident-retirement` 会启动服务端的 Spring 配置并使用其数据源,因此从环境变量和 `config/application.yml` 读取 `SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD` 以及控制面的其他设置;运行它们时需先载入 `server.env`。`csi-protection` 不连接数据库,`resolved-configuration` 也不连接:它打印服务端配置在其运行环境中解析出的 Runtime 状态目录、认证模式和可信 actor 请求头;升级命令以同样的方式运行它来确定 Runtime 状态。每条命令都是一个离线流程,各有前提条件,写在相应的文档中;运行前请先满足这些条件。Hosted Shell 恢复流程仍以 `bin/qwen-managed-agent-operator-recovery` 为入口,`bin/qwen-managed-agent-admin operator-recovery` 运行的是同一条命令。
`bin/qwen-managed-agent-admin <command> [arguments]` 通过 Spring Boot 的 `PropertiesLauncher`,从 server jar 运行各个离线维护入口,例如 `storage-registration`、`workspace-migration` 和各条 CSI 命令。不带参数运行可列出所有命令;某条命令不带参数运行会打印它的用法。与 server 启动脚本一样,当 `config/application.yml` 存在且未设置 `SPRING_CONFIG_ADDITIONAL_LOCATION` 时,它会使用该文件。大多数需要连接数据库的命令使用各自的凭据,而不是 `server.env` 中的 `SPRING_DATASOURCE_*`:Workspace 相关命令读取 `W1_JDBC_URL`、`W1_JDBC_USER` 和 `W1_JDBC_PASSWORD`,CSI 相关命令读取 `K2_JDBC_URL`、`K2_JDBC_USER` 和 `K2_JDBC_PASSWORD`。有两个例外:`operator-recovery` 和 `resident-retirement` 会启动服务端的 Spring 配置并使用其数据源,因此从环境变量和 `config/application.yml` 读取 `SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD` 以及控制面的其他设置;运行它们时需先载入 `server.env`。`csi-protection` 不连接数据库,`resolved-configuration` 也不连接:它打印服务端配置在其运行环境中解析出的 Runtime 状态目录、认证模式和可信 actor 请求头,以及控制面、其健康端点和 Hosted Harness 的 URL;升级命令以同样的方式运行它来确定 Runtime 状态和这些 URL。每条命令都是一个离线流程,各有前提条件,写在相应的文档中;运行前请先满足这些条件。Hosted Shell 恢复流程仍以 `bin/qwen-managed-agent-operator-recovery` 为入口,`bin/qwen-managed-agent-admin operator-recovery` 运行的是同一条命令。
### 升级
@@ -228,6 +228,7 @@ bin/qwen-managed-agent-upgrade verify --tenant <tenant> --turn
- `check` 和 `backup` 从服务端自己的配置中取得 Runtime 状态目录。它们按控制面 unit 运行服务端的方式运行新版本的服务端 jar:使用该 unit 的账号、工作目录和环境;`SPRING_CONFIG_ADDITIONAL_LOCATION` 未设置或为空时,与 launcher 一样默认使用 `config/application.yml`。该 jar 按服务端的方式准备 Spring 环境,因此所有配置文件、import 和 profile 都会生效,`SPRING_APPLICATION_JSON`、`JAVA_TOOL_OPTIONS` 中的系统属性和 relaxed binding 形式的名称也一样;相对路径的目录按 unit 的工作目录解析。它不启动任何组件,也不建立任何连接。如果无法解析配置,或者 unit 给 launcher 传了参数(`systemctl` 打印的参数无法准确还原),`backup` 会拒绝执行,直到用 `--runtime-state <目录>` 指明服务端实际使用的目录,或用 `--runtime-state none` 说明不保存 Runtime 状态。没有配置状态目录时(例如未开启工具路径的部署),`backup` 不归档 Runtime 状态,并明确说明。
- `install --backup <目录>` 拒绝其他版本的备份、内容有变化的备份文件、控制面在备份之后又启动过的备份,以及所导出的数据库与 unit 的 `SPRING_DATASOURCE_URL` 不同的备份。它把新版本复制到 `<分发路径>-releases/<version>`,例如 `/opt/qwen-managed-agent-releases/<version>`(用 `--releases` 指定其他目录,并在每个阶段都传同一个),把已安装版本 `config/` 中的运维文件连同属主和权限复制过去,再用一次 rename 把分发路径指向新版本。副本中属于版本自身的文件(即 `SHA256SUMS` 列出的文件)会设为所有账号可读,所以即使解压时用了严格的 umask,服务账号也能运行该版本。第一次运行会把已安装的目录移到 `<releases>/<旧版本>`,并在原位置放一个链接,因此 unit 中的路径不变。旧版本保留在原处。已存在的 `<releases>/<version>`(例如直接解压到那里的发行包)只有在其 `SHA256SUMS` 与新分发产物相同、且文件与之相符时才会被使用,其中列出的文件同样会设为所有账号可读;复制失败时会删除复制出的内容。如果第一次运行在两次 rename 之间崩溃,分发路径会缺失,旁边留下 `<分发路径>.upgrade-<后缀>`;此后每个阶段都会给出完成安装所需的 `mv -T` 命令。在其他步骤被杀掉的 install,可能在版本目录里留下部分副本 `.<version>.partial-<后缀>`,在分发路径旁留下 `<分发路径>.upgrade-<后缀>` 链接,或在新版本的 `config/` 里留下运维文件的 `.tmp-<后缀>` 副本;`layout` 行会列出它们,下一次 `install` 会删除它们。后缀是 `install` 生成的 8 位十六进制数,因此运维自己的 `<分发路径>.upgrade-backup` 之类不会被删除。版本安装完成后再次运行(例如会话中断后),`install` 会说明已经安装并提示运行 `start`。
- `install` 之前,以及 Harness 的 Broker 配置仍需按下文第 5 步修改时,`start` 会拒绝执行。它先启动 Harness(如果之前某次 `start` 留下了正在运行的 Harness,则重启它,以读入此后修改过的内容),再以服务账号、按 unit 的环境运行 `bin/qwen-managed-agent-server --check`,通过后才启动控制面,并等待 actuator 报告健康(`--timeout`,默认 300 秒;单次探测最多等待 5 秒)。Flyway 在这次启动中迁移 schema。控制面在健康之前退出时会被停止并报告,即使 systemd 的 `Restart=on-failure` 再次运行它就能起来也是如此:迁移 schema 的这次启动中出现了退出,应先查看日志,再重新运行 `start`;超时仍未健康时控制面保持运行,请查看其日志,然后运行 `verify`,或先 `stop` 再重新 `start`。
- `start` 和 `verify` 以同样的方式确定 Harness、控制面及其健康端点的位置,因此在 `server.env` 看不到的地方设置的端口或地址也会生效:配置文件、命令行或 `SPRING_APPLICATION_JSON`。Harness 的位置由 `qwen.managed-agent.harness.base-url` 决定。控制面的位置由 `server.port`、`server.address` 和 `server.ssl` 决定,通配地址经回环接口访问;健康端点遵循 Spring Boot 的 management 设置:单独设置的 `management.server.port` 会把它移到该端口,绑定 `management.server.address`,并位于 `management.server.base-path` 之下。`--server-url <url>` 可改为指定控制面的地址,例如位于代理之后,此时健康端点为 `<url>/actuator/health`。端口为 0 或负数时没有可等待的 URL,因此两个阶段都会拒绝执行,直到用 `--server-url` 指定一个;URL 无法表示的监听(例如带 zone 的 IPv6 链路本地地址)同样会被拒绝。无法解析配置,或者 unit 给 launcher 传了参数时,它们使用 `server.env` 中给出的 URL,并打印一行说明。
- `verify` 以同样方式运行 `--check --online` 并读取 actuator,最多等待 `--timeout`(默认 300 秒)让它应答。指定 `--tenant <id>` 时,它在 `--turn` 创建 Session 之前读取该租户最近的 Session 及其 Item;`--turn` 会创建一个 Session 并等待一个 Turn 完成,这会调用模型,轮询遇到 `5xx` 应答或连接中断时会继续;跳过 Session 读取时,`turn` 行会说明这一点。任何请求都不会超过 `--timeout`。不带 actor 的请求只能列出 Workspace 之外的 Session;`--actor <id>` 通过 `QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER` 指定的请求头带上一个 actor。`verify` 按 `backup` 读取 Runtime 状态目录的方式读取认证模式;使用签名认证时两项读取都跳过,请通过入口读取 Session。
该命令只启动和停止 unit,不会停止入口流量、安装 Qwen Code、修改环境文件或 unit 文件,也不会删除任何版本。`check` 提示 unit 模板有变化时,请在 `start` 之前把变化同步到已安装的 unit,并执行 `systemctl daemon-reload`。
+10 -11
View File
@@ -1,18 +1,18 @@
# Local launch templates for `qwen serve` (v0.16-alpha)
# Local launch templates for `qwen serve`
Reference templates for running `qwen serve` as a long-lived background process on a developer workstation. Pairs with the [v0.16-alpha known limits](./qwen-serve.md#v016-alpha-known-limits) — local-only, single-user, BYO bearer token. Containerized / multi-host / TLS-fronted deployments defer to v0.16.x.
Reference templates for running `qwen serve` as a long-lived background process on a developer workstation. For ordinary local background use, start with the built-in `qwen daemon start` lifecycle commands in the [main guide](./qwen-serve.md#inspect-and-control-a-local-daemon). Use these templates when you need service-manager supervision, startup after reboot, or a custom logging setup. Review the [deployment and security notes](./qwen-serve.md#deployment-and-security-notes) before exposing the listener beyond loopback.
> **Audience**: dogfooding developers who want the daemon up across reboots, with logs going somewhere durable, and a clean `restart-on-failure` story. If you only need the daemon for the duration of a single shell session, plain `qwen serve` (foreground, Ctrl-C to stop) is fine.
> **Audience**: developers who want the daemon up across reboots, with logs going somewhere durable, and a clean `restart-on-failure` story. If you only need the daemon for the duration of a single shell session, plain `qwen serve` (foreground, Ctrl-C to stop) is fine; if you only need a detached local process, `qwen daemon start` is simpler.
## Generate a bearer token (once)
```bash
openssl rand -hex 32 > ~/.qwen-serve-token # user-managed, NOT a built-in path
openssl rand -hex 32 > ~/.qwen-serve-token # example user-managed path
chmod 600 ~/.qwen-serve-token
export QWEN_SERVER_TOKEN="$(cat ~/.qwen-serve-token)"
```
The path / filename is yours to choose; v0.16-alpha does not auto-generate or auto-locate a token file (deferred to v0.16.x). See the [Authentication](./qwen-serve.md#authentication) section of the user guide for the canonical BYO setup.
The path and filename above are examples for these service-manager templates, not a built-in location that Qwen Code discovers automatically. Daemons started through `qwen daemon start` can instead opt into the built-in `--save-token` sidecar and `qwen daemon token` command; see [Inspect and control a local daemon](./qwen-serve.md#inspect-and-control-a-local-daemon). The systemd and launchd templates below still pass the token explicitly because they invoke `qwen serve` directly. See [Authentication](./qwen-serve.md#authentication) for the token-selection rules.
> **Scope this `export` to the current shell session only.** Don't add it to `~/.bashrc` / `~/.zshrc` — a profile-level export exposes the bearer token to every process spawned from that shell (IDE subprocesses, browser debuggers, `npm` scripts from unrelated projects). For long-running setups, use the systemd `EnvironmentFile=` / launchd `EnvironmentVariables` mechanisms below — both scope the token to just the daemon process.
@@ -254,13 +254,12 @@ Service-manager restart semantics differ across the templates:
Within a **single daemon process lifetime**, client disconnects recover via SSE `Last-Event-ID` resume per the [Durability model](./qwen-serve.md#durability-model) section of the user guide — the replay ring is in-memory.
A daemon **restart** drops all in-memory sessions; clients reconnect and start fresh. Cross-restart durability of session content (prompts, tool calls, conversation history) is **NOT** in v0.16-alpha.
A daemon **restart** interrupts every in-flight live session and clears the in-memory replay ring. Persisted session content remains on disk and can be loaded against the new daemon process, subject to the same workspace binding rules. See [Loading and resuming a persisted session](./qwen-serve.md#loading-and-resuming-a-persisted-session) for the available recovery routes.
## Out of scope (defers to v0.16.x or later)
## Scope and related deployment guidance
- **Containerized deployment** — Dockerfile, docker-compose, Kubernetes manifests, nginx + TLS reverse proxy, multi-instance token isolation. Defers to v0.16.x once an enterprise pilot is committed; the doc would otherwise rot from no-one-validating.
- **Cross-host federation / multi-daemon coordination on one host** — one daemon can host multiple registered workspace runtimes, but daemons do not coordinate. Instance-path token keying + stale-token cleanup defer to v0.16.x.
- **General daemon token storage** — Local Control uses revocable daemon-owned pairing tokens, but long-lived runtime token storage remains BYO-token. Persistent token-store infrastructure defers to v0.16.x.
- **Containers and reverse proxies** — this page only provides local service-manager templates. For listener, browser-origin, and certificate requirements, see [Remote Web Shell and phone access](./qwen-serve.md#remote-web-shell-and-phone-access) and [HTTPS / TLS](./qwen-serve.md#https--tls-for-mobile--cross-device-access). Multi-tenant deployments require an external orchestrator with separate daemons for independent security principals.
- **Cross-host federation / multi-daemon coordination** — one daemon can host multiple registered workspace runtimes, but daemons do not coordinate session state with each other.
- **Windows native service** (`nssm`, Service Control Manager wrapper) — for now use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/) and follow the systemd section above.
See the [v0.16-alpha known limits](./qwen-serve.md#v016-alpha-known-limits) callout in the main user guide for the full deferred-features list, and [#4175](https://github.com/QwenLM/qwen-code/issues/4175) for the v0.16-alpha rollout tracking issue.
See the [deployment and security notes](./qwen-serve.md#deployment-and-security-notes) and [Authentication](./qwen-serve.md#authentication) in the main user guide for the current security model and token options.
+21 -35
View File
@@ -1,12 +1,8 @@
# Daemon mode (`qwen serve`)
Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs, CI scripts, custom CLIs) share one agent session over HTTP + Server-Sent Events instead of each spawning their own subprocess.
Run Qwen Code as an HTTP daemon so multiple clients (IDE plugins, web UIs, CI scripts, custom CLIs) can share agent sessions over HTTP + Server-Sent Events instead of each spawning their own subprocess.
> **🚧 v0.16-alpha**: `qwen serve` first ships to npm in v0.16-alpha as **text-only chat / coding** with **local-only deployment**. Image / file attachments on the prompt path, containerized deployment (Docker / k8s / nginx reverse-proxy), and remote / multi-daemon hardening land in a follow-up patch when an enterprise pilot is committed. See [v0.16-alpha known limits](#v016-alpha-known-limits) for the full deferred list.
> **Status:** Stage 1 (experimental). The protocol surface is locked at the §04 routes table from issue [#3803](https://github.com/QwenLM/qwen-code/issues/3803). Stage 1.5 (`qwen --serve` flag — TUI co-hosts the same HTTP server) and Stage 2 (in-process refactor + `mDNS`/OpenAPI/WebSocket/Prometheus polish) are immediately downstream.
>
> **Scope honesty:** Stage 1 is sized for **developers prototyping clients against the protocol surface** and for **local single-user / small-team collaboration**. Production-grade multi-client / long-running / network-flaky workloads (mobile companions, IM bots reaching 1000+ chats) need Stage 1.5+ guarantees that aren't in this release. See [Stage 1.5+ runtime guarantees](#stage-15-runtime-guarantees) for the full gap list and #3803 for the convergence roadmap.
> **Status:** Experimental. The daemon supports local and remote Web Shell/API deployments. Before exposing it beyond loopback, review [Authentication](#authentication) and the [deployment threat model](#default-deployment-threat-model).
## What it gives you
@@ -25,34 +21,13 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs,
- **Known limit — token-cost amplification:** the route is a pure-cost endpoint (each call is an LLM side-query, no state benefit) and the daemon has no per-route rate limit in v1. On a no-token loopback default a buggy or malicious local client can spam it to burn tokens. Configure `--token` (and optionally `--require-auth`) on shared dev hosts before exposing the daemon.
- **Concurrent recap safety:** two simultaneous `/recap` calls on the same session run two independent side-queries. `generateSessionRecap` reads a snapshot of the chat history via `GeminiClient.getChat().getHistory()` and feeds it to a separate `BaseLlmClient.generateText` call (via `runSideQuery`); it never appends to or mutates the session's `GeminiChat`. Safe to call from multiple clients without coordination.
## v0.16-alpha known limits
## Deployment and security notes
The first npm release of `qwen serve` (v0.16-alpha) is intentionally narrow — text-only chat / coding for developers running the daemon on their own machine. The list below makes the deferred surface explicit so adopters can plan around it; everything here is on the v0.16.x patch roadmap or a near-term follow-up release.
**Product surface — text-only:**
- ✅ Text prompts and text responses (chat, coding, tool calls, MCP integration)
- ❌ **Image / file attachments on the prompt path** — `MessageEmitter` currently only renders text; multimodal echo lands when an alpha target with image needs is committed (#4175 chiga0 #27 P0 item)
- ❌ **Streaming uploads** — same gating as multimodal
**Deployment surface — local-only:**
- ✅ Loopback (`127.0.0.1`, default) — no auth required, suitable for dev workstations
- ✅ Local launch via `systemd` / `launchd` / `nohup &` / `tmux` — see [Local launch templates](./qwen-serve-deploy-local.md)
- ✅ Bring-your-own bearer token via `QWEN_SERVER_TOKEN` env var ([Authentication](#authentication) for setup)
- ❌ **Containerized deployment** — Docker / Compose / Kubernetes / nginx reverse-proxy with TLS termination NOT in v0.16-alpha. Defers to v0.16.x once an enterprise pilot is committed (would otherwise rot from no-one-validating).
- ❌ **Multi-daemon coordination on one host** — one daemon can host several explicitly registered workspaces, but daemons do not coordinate with each other. Cross-host federation, instance-path token keying, and stale-token cleanup defer to v0.16.x.
- ✅ **Revocable Local Control pairing tokens** — `--local-control` mints a separate LAN pairing token owned by the daemon. General daemon token storage remains BYO-token.
**Hardening — minimum viable for local single-user:**
- ✅ Boot-time security gate (non-loopback binds always carry a bearer token — operator-supplied or generated at startup; explicitly empty sources still refuse to boot, [PR 15 / #4236](https://github.com/QwenLM/qwen-code/pull/4236))
- ✅ Mutation-route auth gate, session-scoped permission routing (Wave 4 PRs)
- ✅ MCP guardrails + multi-client permission coordination (F2 / F3)
- ✅ **Prompt absolute deadline + SSE writer idle timeout** — opt-in via `--prompt-deadline-ms` and `--writer-idle-timeout-ms`; advertised through `prompt_absolute_deadline` and `writer_idle_timeout` when enabled.
- ✅ **HTTP rate limiting** — opt-in via `--rate-limit` and per-tier thresholds; advertised through `rate_limit` when enabled.
- ⏸️ **Prometheus metrics + load test harness** — defers to v0.17 F4 Phase-1 scale instrumentation when 30-50 active sessions becomes a real target.
- ⏸️ **`--max-body-size` CLI flag** — daemon enforces `express.json({ limit: '10mb' })` by default which comfortably covers text-only prompts (model context windows are well under 10 MiB of chars). Tunable via flag in v0.16.x.
- **Loopback is trusted by default** — `127.0.0.1:4170` starts without bearer authentication, and every local process that can reach it receives full API authority, including code execution as the daemon user. Use a stable `QWEN_SERVER_TOKEN` with `--require-auth` on shared or untrusted hosts.
- **Non-loopback listeners always carry a bearer token** — supply your own token or use the ephemeral token printed at startup. Generated tokens rotate on restart, so reusable clients need a stable token. HTTP is unencrypted; configure TLS before crossing untrusted networks.
- **Background launch has two paths** — use `qwen daemon start` for built-in local lifecycle management, or the [systemd / launchd / tmux / nohup templates](./qwen-serve-deploy-local.md) when you need service-manager supervision or startup after reboot.
- **Token persistence is opt-in** — `qwen daemon start --save-token` stores an explicitly supplied bearer in an owner-only sidecar for `qwen daemon token`; ordinary `qwen serve` and generated tokens do not use that sidecar. See [Inspect and control a local daemon](#inspect-and-control-a-local-daemon).
- **Multi-workspace is single-operator** — registered workspaces share the daemon's bearer token, listener, quotas, and process fault boundary. Use separate daemons and an external orchestrator for independent users or security principals; see [Multi-session & multi-workspace deployment](#multi-session--multi-workspace-deployment).
For the deeper "what we won't fix in Stage 1" enumeration (single-host session-state mutation model + N parallel sessions sharing one ACP child inside each workspace runtime), see [Stage 1 scope boundaries](#stage-1-scope-boundaries--what-we-wont-fix-in-stage-15) below.
@@ -142,6 +117,17 @@ Records live under the runtime directory (`$QWEN_RUNTIME_DIR`, `$QWEN_HOME`, or
`qwen daemon start` spawns `qwen serve` detached, waits for its discovery record, and prints the bound URL, PID, log path, and stop command. It drops the launching session's identifiers and private capability bindings so the child is a fresh daemon, keeps the operator's model/auth environment, and passes `--no-token-qr` so the startup log cannot contain a token. Workspaces are canonicalized, relative runtime directories are pinned to the launching directory before spawn, and Windows drive-relative roots are resolved against the launching drive. A non-loopback bind requires `QWEN_SERVER_TOKEN`; use `qwen serve` directly if you want an ephemeral token. The startup log is `0600` and is removed after a successful start. `--wait` waits until the runtime is serving (no `daemon_runtime_starting` issue and not `error`); a timeout keeps the startup log and exits non-zero.
For an authenticated background daemon with a reusable local credential, supply a stable token through the environment and opt in to the token sidecar:
```bash
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen daemon start --require-auth --save-token --wait
qwen daemon token --port 4170 # print the saved bearer; treat the output as a secret
qwen daemon token --port 4170 --remove # delete only the local sidecar
```
Prefer `QWEN_SERVER_TOKEN` over `--token`, which is visible in the shell process list. `--save-token` requires an explicitly supplied token and refuses to spawn without one. The sidecar is `daemon/daemon-<port>-<pid>.token` under the same runtime directory as the discovery record; it is `0600` on POSIX and is never included in the discovery record or daemon log. `qwen daemon stop` removes it, while `qwen daemon restart` carries it to the replacement instance. `qwen daemon token --remove` deletes the local copy but does not change or revoke the running daemon's credential; stop and start the daemon with a new token to rotate it. Ordinary `qwen serve`, including `--open-with-auth`, does not write this sidecar.
`qwen daemon restart` stops one registered daemon through `daemon stop` and starts it again through `daemon start --wait` on its recorded port and hostname, with its recorded startup workspaces, Web Shell mode, and `--require-auth` setting; `--web`/`--no-web` and `--require-auth`/`--no-require-auth` override the record. Persisted workspace registrations are restored by the new daemon itself, not replayed as `--workspace`. Every other `qwen serve` option (for example `--profile`, `--channel`, `--allow-origin`, or rate limits) resets to its default, so relaunch a daemon configured with those through `qwen serve`. Before stopping anything, restart refuses an HTTPS daemon, a startup workspace that no longer exists, a run from inside a shell the daemon hosts (such as its Web Shell terminal), and a bearer daemon without `--token` or `QWEN_SERVER_TOKEN`; a running bearer daemon must accept the token, so restart never rotates the credential. A token kept only in a workspace `.env` or settings file is not read by `qwen daemon`. `--force` is forwarded to `daemon stop` only when you pass it. If the recorded port is taken, the daemon starts on the next free port and restart exits non-zero.
### Workspace registration capacity
@@ -948,7 +934,7 @@ Note: live-session history replay is bounded twice: by the SSE ring for `Last-Ev
## Durability model
**Sessions are still ephemeral in Stage 1 across daemon restarts**, but persisted sessions on disk can be reloaded:
**Live sessions are interrupted by daemon restarts, but persisted sessions remain on disk and can be reloaded:**
- A child process crash publishes `session_died` and removes the live session from the daemon's maps. The persisted on-disk session **can** be reloaded via `POST /session/:id/load` if a fresh agent child is spawnable.
- A daemon restart loses every in-flight live session. The persisted sessions remain on disk and can be loaded against a new daemon process, subject to the same workspace binding rules.
@@ -1289,7 +1275,7 @@ Budget-related events from the existing `mcp_guardrail_events` surface (`mcp_bud
## What's next
- **Setting up a long-running daemon?** [Local launch templates (systemd / launchd / nohup / tmux)](./qwen-serve-deploy-local.md) for v0.16-alpha (local-only).
- **Setting up a long-running daemon?** Use the [local launch templates (systemd / launchd / nohup / tmux)](./qwen-serve-deploy-local.md) for service-manager supervision and startup after reboot.
- **Build a client?** See the [DaemonClient TypeScript quickstart](../developers/examples/daemon-client-quickstart.md) and the [HTTP protocol reference](../developers/qwen-serve-protocol.md).
- **Reading the source?** Bridge code lives at `packages/cli/src/serve/`; SDK client at `packages/sdk-typescript/src/daemon/`.
- **Tracking the roadmap?** Stage 1.5 / Stage 2 progress is tracked on issue [#3803](https://github.com/QwenLM/qwen-code/issues/3803).
+15 -11
View File
@@ -42,23 +42,27 @@ capability or option. A failed write leaves the published mode unchanged.
## Web Shell and VS Code
The composer in Web Shell, split panes, and the modern VS Code chat includes
an independent **Workflow** control for supported sessions. The switch shows
the saved preference, not whether the mode can currently take effect. If the
preference is on but the tools are unavailable, it stays checked and can still
be turned off. Click the Workflow label or focus and activate it to open its status details;
the same details are available by tapping on touch devices.
The composer in Web Shell, split panes, and the modern VS Code chat lists
**Workflow** in its **+** menu for supported sessions. The check mark shows the
saved preference, not whether the mode can currently take effect; on wider
screens a second line summarizes what the mode does or its current state. While
the preference is on, the toolbar shows a **Workflow** chip; its tooltip carries
the status details, and clicking it turns the mode off. If the preference is on
but the tools are unavailable, the entry stays checked, the chip is shown in the
warning color, and both can still turn the mode off.
Before a session is created or restored, the control shows an availability
placeholder. It does not create a session or save a future preference. Hosts
that do not advertise support, including older servers, hide the mode switch.
that do not advertise support, including older servers, hide the entry. Hosts
that hide the **+** menu show neither the entry nor the chip.
Changes from another client update the same session's control automatically.
During a write or reconnection, the last confirmed value remains visible and
the switch is disabled until state is confirmed. A timeout is an unconfirmed
outcome, not a successful change; the client reads the current state instead
of automatically repeating the write. An active response does not disable the
switch: the change applies to the next fresh turn and leaves effort unchanged.
the entry and chip are disabled until state is confirmed. A timeout is an
unconfirmed outcome, not a successful change; the client reads the current
state instead of automatically repeating the write. An active response does
not disable the entry or chip: the change applies to the next fresh turn and
leaves effort unchanged.
## Reminder and input behavior
+10 -8
View File
@@ -33,18 +33,20 @@ Managed、SSH、派生/子代理、session-agent 及关闭聊天录制的会话
## Web Shell 与 VS Code
Web Shell、分屏和现代 VS Code 聊天的输入栏为受支持会话提供独立的 **Workflow**
控件。开关表示已保存的偏好,而不是当前是否实际生效。偏好已开启但工具不可用时,
开关仍保持选中,并允许关闭。点击 Workflow 标签可打开状态详情;键盘聚焦后激活标签
或在触屏设备上轻点,也能查看相同详情。
Web Shell、分屏和现代 VS Code 聊天的输入栏为受支持会话在 **+** 菜单中提供
**Workflow** 条目。勾选表示已保存的偏好,而不是当前是否实际生效;较宽的屏幕上,
条目第二行说明该模式的作用或当前状态。偏好开启期间,工具栏显示 **Workflow**
标签,悬停提示给出状态详情,点击即可关闭。偏好已开启但工具不可用时,条目仍保持
勾选,标签以警告色显示,两者都能关闭该模式。
尚未创建或恢复会话时,控件只显示可用性占位提示,不会创建会话或保存将来的偏好。
未广告支持能力的宿主(包括旧版服务端)不显示模式开关。
未广告支持能力的宿主(包括旧版服务端)不显示该条目;隐藏 **+** 菜单的宿主
既不显示条目,也不显示标签。
其他客户端的更改会自动同步到同一会话的控件。写入或重连期间保留最后确认值,
直到状态重新确认前禁用开关。超时表示结果尚未确认,不是修改成功;客户端读取当前
状态,而不会自动重复写入。正在输出回复不会禁用开关:更改对下一轮新输入生效,
并保持思考强度不变。
直到状态重新确认前禁用条目和标签。超时表示结果尚未确认,不是修改成功;客户端
读取当前状态,而不会自动重复写入。正在输出回复不会禁用条目或标签:更改对下一轮
新输入生效,并保持思考强度不变。
## 提醒与输入行为
@@ -32,6 +32,10 @@ import {
} from '../fake-openai-server.js';
import { ACP_HOME_PREFIX, removeScratchDir } from '../scratch-dir.js';
// The agent trusts a client that presents the capability it was started with.
const PRIVATE_ACP_CAPABILITY_ENV = 'QWEN_CODE_PRIVATE_ACP_CAPABILITY';
const PARENT_CAPABILITY = 'acp-agent-loop-parity-parent';
type JsonObject = Record<string, unknown>;
interface Scenario {
@@ -41,6 +45,13 @@ interface Scenario {
settings?: JsonObject;
/** Answers each `session/request_permission`; allows once by default. */
permission?: (request: JsonObject) => JsonObject;
/** The `_meta` each prompt is sent with, by index. */
promptMeta?: Array<JsonObject | undefined>;
/**
* The client holds the private parent capability, as the channel bridges
* do, so the agent keeps trusted prompt metadata such as a channel mark.
*/
trustedParent?: boolean;
/** Sends `session/cancel` once this file exists in the test directory. */
cancelOnFile?: string;
/**
@@ -260,6 +271,10 @@ const allowOnce = () => ({
};
delete env['QWEN_CODE_ENABLE_UNIFIED_AGENT_LOOP'];
if (shared) env['QWEN_CODE_ENABLE_UNIFIED_AGENT_LOOP'] = '1';
delete env[PRIVATE_ACP_CAPABILITY_ENV];
if (scenario.trustedParent) {
env[PRIVATE_ACP_CAPABILITY_ENV] = PARENT_CAPABILITY;
}
const agent = spawn(
'node',
[rig.bundlePath, '--acp', '--no-chat-recording'],
@@ -378,13 +393,18 @@ const allowOnce = () => ({
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
...(scenario.trustedParent && {
_meta: {
'qwen-code/private-parent-capability': PARENT_CAPABILITY,
},
}),
});
await request('authenticate', { methodId: 'openai' });
({ sessionId } = (await request('session/new', {
cwd: dir,
mcpServers: [],
})) as { sessionId: string });
for (const text of scenario.prompts) {
for (const [promptIndex, text] of scenario.prompts.entries()) {
let settled = false;
const cancelFile = scenario.cancelOnFile;
if (cancelFile) {
@@ -400,6 +420,9 @@ const allowOnce = () => ({
const result = (await request('session/prompt', {
sessionId,
prompt: [{ type: 'text', text }],
...(scenario.promptMeta?.[promptIndex] && {
_meta: scenario.promptMeta[promptIndex],
}),
})) as JsonObject;
stopReasons.push(result['stopReason']);
} catch (error) {
@@ -592,6 +615,41 @@ const allowOnce = () => ({
prompts: ['First question.', 'Second question.'],
reply: (messages) => ({ content: `Answer ${userTurns(messages)}.` }),
},
{
name: 'a retry after a model error',
prompts: ['Answer, please.', 'Answer, please.'],
// The client retries the prompt the error ended.
promptMeta: [undefined, { 'qwen.daemon.retry': true }],
reply: (_messages, _dir, index) =>
index === 0
? { errorContent: 'upstream exploded' }
: { content: 'Here is the answer.' },
},
{
name: 'a retry that fails again',
prompts: ['Answer, please.', 'Answer, please.', 'Answer, please.'],
// The second error comes after the retry's request reached history.
promptMeta: [
undefined,
{ 'qwen.daemon.retry': true },
{ 'qwen.daemon.retry': true },
],
reply: (_messages, _dir, index) =>
index < 2
? { errorContent: 'upstream exploded' }
: { content: 'Here is the answer.' },
},
{
name: 'a channel prompt that reads a file',
prompts: ['Read the readme.'],
promptMeta: [{ 'qwen.channel.prompt': true }],
trustedParent: true,
prepare: (r) => r.createFile('readme.txt', 'readme body\n'),
reply: (messages, dir) =>
toolResults(messages) === 0
? { toolCalls: [read(join(dir, 'readme.txt'), 'call_read')] }
: { content: 'It says readme body.' },
},
{
name: 'a model error',
prompts: ['Fail please.'],
Loaded 100 of 618 files, more files were not shown because too many files have changed in this diff. Show more