diff --git a/.github/workflows/source-release.yml b/.github/workflows/source-release.yml index 4055f9c3..da821114 100644 --- a/.github/workflows/source-release.yml +++ b/.github/workflows/source-release.yml @@ -94,7 +94,7 @@ jobs: working-directory: ${{ env.SOURCE_ROOT }}/electron run: >- node -e "require('node:child_process').execFileSync(require('electron'), - ['scripts/smoke-settings-upgrade.cjs', 'v0.15.0-alpha.0'], + ['scripts/smoke-settings-upgrade.cjs', 'v0.15.2-alpha.0'], {stdio: 'inherit', windowsHide: true, timeout: 45000})" - name: Package extracted Windows sources diff --git a/README.md b/README.md index 59615400..b70ac49e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@
-
+
@@ -51,7 +51,7 @@ the Host owns identity, state, permissions, persistence, and recovery.
> [!IMPORTANT]
> This repository contains buildable, runnable source. This branch targets
-> **0.15.2 Alpha**, not a packaged desktop release.
+> **0.16.0 Alpha**, not a packaged desktop release.
> Amadeus first-party code is open-source under the
> [GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE).
> Third-party code and external assets retain their own terms.
diff --git a/README_ZH.md b/README_ZH.md
index 9b9e0ad4..3599b661 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -10,7 +10,7 @@
-
+
@@ -49,7 +49,7 @@ Amadeus 试图把这些体验连成一个闭环:
持久化与恢复。
> [!IMPORTANT]
-> 本仓库包含可构建、可运行的公开源码,本分支为 **0.15.2 Alpha 候选版**,
+> 本仓库包含可构建、可运行的公开源码,本分支为 **0.16.0 Alpha**,
> 不是带安装器的正式桌面发行版。Amadeus 第一方代码依据
> [GNU Affero General Public License v3.0(AGPL-3.0)](LICENSE) 开源。
> 第三方代码与外部资产保留各自条款。
diff --git a/docs/alpha-0.16-acceptance.md b/docs/alpha-0.16-acceptance.md
new file mode 100644
index 00000000..190d4499
--- /dev/null
+++ b/docs/alpha-0.16-acceptance.md
@@ -0,0 +1,159 @@
+# 0.16.0 Alpha acceptance record
+
+Date: 2026-10-11 (Pacific/Auckland). Preparation baseline: `09aecfb`, including
+merged #173 and #174. Comparison release: `v0.15.2-alpha.0`.
+
+Release preparation uses a clean, isolated Git worktree. Uncommitted local voice
+configuration, private runtime state and external media are excluded.
+
+## Automated and source-package validation
+
+The first preparation commit, `f6bb00c`, passed all 17 PR checks across eight
+workflows: Python Windows, Electron Windows, Python Linux, Python macOS, local
+model installation candidates, Windows ROCm candidate, optional character RAG,
+and Source Release. The Source Release job installs locked dependencies, builds
+and packages Electron, verifies native settings upgrade, and launches the
+unpacked executable using the extracted source ZIP. Final publication must use
+the final clean commit and its corresponding manifest/checksum.
+
+Local checks with Python 3.12.10 and Node 22:
+
+- Electron: **388/388** tests and TypeScript/Vite production build passed after
+ the compatible dependency repairs. The existing bundle-size advisory remains.
+- Full isolated Python runner executed all discovered suites. Its retained raw
+ summary was 5358 passed / 20 skipped and a failing exit: `test_system_settings`
+ had 17 failures and `test_tts_emotion_references` had one. Both failures came
+ from test-created external-asset junctions while audio qualification was being
+ prepared. The product correctly rejected paths outside the asset root.
+ The junctions were removed; settings plus installed Pi runtime/web contracts
+ then passed **48**, with one optional skip; emotion-reference, Codex approval
+ cancellation and semantic-evidence tests passed **80**. The separate clean CI
+ run passed all three full-suite shards. The failed local run is not relabeled
+ as a green full run. Its aggregate collection and per-suite outcome counts do
+ not exactly reconcile, so no combined final test-count claim is derived.
+- Environment verifier, Ruff, generated architecture views, whitespace checks
+ and third-party provenance release gate passed.
+- Clean source archive: **3882 files**, zero policy errors or warnings at the
+ first preparation commit. Final release hashes come from the final manifest.
+
+## Native desktop and upgrade
+
+- Shipping model-less Electron launch: **11/11** checks passed, covering backend
+ startup, authentication, navigation, configuration visibility and clean exit.
+- Actual settings from `v0.15.2-alpha.0`, with a synthetic native-encrypted
+ credential, survived upgrade and reopening. No personal profile was migrated.
+- Native wallpaper composer and offline startup-settings GUI checks passed;
+ **13** Windows lifecycle contracts passed. The composer process emitted two
+ Chromium GPU-state diagnostics while exiting; its interaction assertions
+ passed. This is not a long-duration GPU or real Lively qualification.
+- Extended native role journey: **17/17** checks passed. Two same-name roles
+ retained distinct IDs; pending selection did not change the running identity;
+ restart applied selection and edits; malformed-role recovery returned to
+ Kurisu while preserving the malformed user file; Chat reconnected and owned
+ processes exited. An initial local harness used the wrong role-directory
+ environment variable; a second attempt overlapped the asset-junction setup.
+ Both unsuccessful attempts are retained as instrument/setup failures. The
+ final run used the documented directory setting and clean model-less assets.
+ The six extra role assertions use a task-local extension; the ordinary shipped
+ smoke alone is claimed to cover only its original eleven assertions.
+
+## Real models, actions and device playback
+
+- Codex App Server 0.154.0: native **allow and deny each passed 8/8** assertions.
+ Allow continued the same WorkItem/Attempt and wrote the requested synthetic
+ file. Deny persisted refusal, closed the permission card and left it absent.
+ The first isolated attempts lacked the credential helper's explicit env-file
+ binding; those authentication failures were retained. Corrected tests used
+ the existing binding and disabled desktop-provider synchronization. The
+ initial test's managed non-secret provider path was restored afterward.
+- Real Chat and Codex Work control: **10/10** assertions passed with the project
+ opened through the normal Session API. The initial request started one run;
+ progress inquiry was read-only; amendment delivery was recorded for that same
+ run; the final file contained exactly the amended text; WorkItem and Attempt
+ identity remained unchanged. The old harness first asked for a context switch
+ but never answered the resulting selection, then waited for the retired
+ `steer_queued` event despite an existing delivered input receipt. These failed
+ attempts remain recorded. The probe now checks the durable delivery receipt
+ and actual file result. This does not certify an unattended compound
+ context-switch-and-execute request or restore the obsolete event.
+- Shipping Electron, DeepSeek V4 Flash and seeded AUIP Gomoku: **37/37**
+ structural checks passed. The journey played a complete round through real
+ player clicks and model actions, restarted, resigned, concluded, left the app
+ and returned to ordinary Chat. Accepted receipts, actor/app identity, bounded
+ experience history and the post-leave conversation were verified. The final
+ screenshot and role replies were inspected. This run deliberately disabled
+ TTS; the seeded application is a fixture, not Provider-generated software.
+- Actual local GPT-SoVITS v3 checkpoint, Python 3.12.10, Torch 2.6.0+cu124 and
+ NVIDIA RTX 4070 Ti SUPER: one warmup plus four short VN replies produced nonzero
+ PCM written to the physical output device through the production VN bridge
+ and shared speech pipeline. Existing external voice assets and the installed
+ GPU environment were used with the candidate source in a separate extracted
+ directory. No media or machine configuration enters the source archive.
+
+| Delivery | First successful device write | PCM peak |
+| --- | ---: | ---: |
+| Whole speech body | 2.463 s | 0.349 |
+| First segment | 1.962 s | 0.520 |
+| First segment | 1.765 s | 0.298 |
+| Whole speech body | 1.682 s | 0.353 |
+
+The old audio probe observed only the streaming method and missed complete-audio
+playback. It now observes successful writes on the actual `PyAudio.Stream` class
+used by `PyAudio.open`, covering both delivery paths. The old public `Stream`
+alias is a different class in the installed PyAudio. Failed instrument runs were
+not counted as passes. These timestamps measure device submission, not acoustic
+arrival; there is no new latency target, percentile or regression conclusion.
+The maintainer deferred a matched previous-release latency comparison for this
+release. Human microphone recognition and subjective listening remain unverified.
+
+Reproduction entry points (use isolated profiles and configured credentials):
+
+```text
+python -X utf8 tools/run_tests.py
+npm test # electron/
+npm run build # electron/
+python -X utf8 tools/smoke_electron_model_less.py
+python -X utf8 tools/e2e_codex_app_server_permission.py --decision allow_once
+python -X utf8 tools/e2e_codex_app_server_permission.py --decision deny
+python -X utf8 tools/e2e_codex_app_server_control.py
+python -X utf8 tools/probes/measure_vn_audio_latency.py --live-model-and-audio --baseline-delivery whole-speak
+```
+
+The live Codex tests need `CODEX_APP_SERVER_PROVIDER_AUTH_ENV_FILE` to refer to
+the configured credential source; set `CODEX_APP_SERVER_SYNC_DESKTOP_PROVIDER=0`
+for isolated tests. Never log or publish that file. The audio command plays real
+audio and requires the optional GPU/voice dependencies and separately installed
+assets. It must not share asset setup with a clean model-less test run.
+
+## Dependency audit disposition
+
+Compatible Electron tooling repairs update concurrently 10.0.5 to 10.0.6
+(shell-quote 1.9.0 to 1.12.0), http-cache-semantics 4.2.0 to 4.3.0, and joi
+18.2.8 to 18.2.9. Electron has no remaining high/critical audit findings, but
+eight moderate build-tool findings remain in the electron-builder/global-agent/
+roarr/sprintf-js chain. No force downgrade or broad audit suppression was added.
+
+Pi remains pinned to its qualified 0.86.1 runtime. Its upstream shrinkwrap
+installs brace-expansion 5.0.9, retaining the denial-of-service advisories
+[GHSA-q2hr-2g5m-vwhr](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr),
+[GHSA-qhr7-859c-m2p7](https://github.com/advisories/GHSA-qhr7-859c-m2p7), and
+[GHSA-6j4f-fj2g-mc7p](https://github.com/advisories/GHSA-6j4f-fj2g-mc7p).
+The audit groups these as one high-severity vulnerable package. A root override
+did not repair it. An outer lockfile-only change made the audit green while a
+fresh install still contained 5.0.9; that ineffective change was discarded.
+Upstream 0.87.1's inspected package also retains 5.0.9. This remains a disclosed
+dependency limitation, not a clean Pi audit or a claim of unreachable risk.
+
+Python core/development audit reported no findings after the existing narrow CI
+exemption (`PYSEC-2026-1845`; the tool reported two ignored advisory identities).
+The project itself is not audited as a PyPI package. Optional GPU dependencies
+were not upgraded or assigned a new vulnerability-free claim.
+
+## Evidence boundaries
+
+Deterministic suites, native desktop startup, extracted source installation,
+real-model interaction, device playback and human microphone/listening checks
+establish different facts. Missing optional dependencies and manual checks are
+reported as skipped or unverified, never counted as passes. Raw logs, local
+paths, credentials, conversations and runtime media remain outside the source
+release; only sanitized aggregate results are recorded here.
diff --git a/docs/alpha-0.16.md b/docs/alpha-0.16.md
new file mode 100644
index 00000000..329972ad
--- /dev/null
+++ b/docs/alpha-0.16.md
@@ -0,0 +1,99 @@
+# 0.16.0 Alpha: unified Chat and local character management
+
+Tag: `v0.16.0-alpha.0`; Python `0.16.0a0`; Electron `0.16.0-alpha.0`.
+This is a source Alpha release. Model weights, character media, reference voices,
+Live2D Cubism Core, credentials and desktop installers are not bundled.
+The comparison baseline is `v0.15.2-alpha.0`.
+
+## Changes
+
+- **One Chat runtime.** Cooperative owns production Chat in both professional
+ and basic mode. Shared presentation retains streaming, expressions and speech;
+ the old Original orchestration path is retired. Provider handoffs preserve
+ shared constraints without expanding the assigned task's scope.
+- **Local character management.** Settings can create and edit local roles,
+ select the next startup identity, and recover explicitly from an invalid role.
+ Conversations remain owned by their role; accepted Work retains its identity.
+ Settings groups identity, appearance and the existing Kurisu knowledge controls
+ under Characters, with voice configuration in its single Voice editor.
+- **Experimental Live2D.** Optional local Live2D models use the existing render
+ and wallpaper path, including expression and mouth signals. Sprite remains
+ available. Users supply the model and Cubism Core under their respective terms.
+- **Desktop and rendering.** Wallpaper and Render transitions share serialized
+ lifecycle ownership. Texture residency is bounded and BC7 frames can be cached.
+ SpriteForge framing uses the pack canvas; wallpaper preserves aspect ratio.
+ Connection editors have consistent save state and dark Render backgrounds.
+- **Voice and history.** Speech aggregation accounts for ready audio and the
+ selected synthesis profile; idle output streams persist between turns.
+ Continuous voice can return to wake standby, Qwen3-ASR can remain in system
+ memory, and BERT loads only for Chinese GPT-SoVITS text. V3 emotion references
+ remain opt-in. Sessions retain complete history while Main Chat receives a
+ bounded recent window.
+- **Configuration and recovery.** Setting declarations and built-in handler
+ registration share one catalog. Durable state writes, runtime diagnostics and
+ translation checks are strengthened. Restoring Watching mode preserves an
+ explicitly disabled vision setting.
+
+## Install and upgrade
+
+Use the source ZIP and the installation profile in [README](../README.md).
+Extract into a new directory and run `uv sync --locked` with the complete set of
+extras for your profile, then `npm ci` and `npm run build` in `electron/`.
+Running only the core install command against an existing optional-model
+environment removes extras; retain your full profile selection.
+
+Before upgrading, stop incoming turns and drain or stop owned Work. Back up your
+settings and persistent data. Keep external assets separate and configure their
+locations in Settings. Do not copy caches or an entire old virtual environment.
+
+The retired `COOPERATIVE_CHAT_ENABLED=false` no longer selects Original Chat.
+Settings explains and acknowledges migration; an obsolete `.env` assignment
+must be removed from that file. `COOPERATIVE_WORK_PLANNER_ENABLED=false` still
+selects basic Cooperative. There is no hidden Original fallback.
+
+Pure-local Chat uses `llama_server`; persistent `cli` sessions require migration.
+The managed pure-local context defaults to 16384, while Hybrid keeps 4096.
+Explicit saved values are preserved. Hybrid now uses its configured local head
+alongside the remote role model. See [runtime migration](chat-runtime-convergence.md).
+
+Role selection applies after restart. Appearance and voice remain application-wide;
+creating a role does not create a separate voice, artwork or memory library.
+See [character management](character-management.md) and
+[Live2D setup](live2d_character_visuals.md).
+
+For rollback, use a separate checkout or source directory of `v0.15.2-alpha.0`.
+Do not overwrite newer history or Work receipts with an old database snapshot.
+Acknowledging removal of the retired route setting does not restore that setting
+when older software is started. Unknown external Provider outcomes remain unknown.
+
+## Validation and known limits
+
+The [0.16 acceptance record](alpha-0.16-acceptance.md) distinguishes this release's
+checks from earlier feature evidence. Alpha does not imply that every model,
+device, operating system or experimental surface has been qualified.
+
+- Model-less CI does not establish microphone recognition, audible quality,
+ acoustic latency, GPU inference or long-duration resource stability.
+- Browser cross-domain journeys and pure-local model task interpretation retain
+ recorded limitations; LM Studio/Ollama capacity behavior remains unqualified.
+ See the [runtime acceptance boundaries](chat-runtime-convergence.md#evidence-and-remaining-acceptance).
+- Live2D and VN remain experimental. Live2D model physics/motions and external
+ wallpaper hosts depend on the supplied assets and environment.
+- Per-role voice/appearance bindings, automatic asset installation and general
+ cross-session semantic memory are not part of this release.
+- Platform and optional GPU-profile support remains limited to the evidence
+ documented in [installation profiles](install_profiles.md).
+- Dependency audits are not clean: the pinned Pi runtime retains a known
+ brace-expansion denial-of-service risk, and Electron build tooling retains
+ moderate findings. See the [exact audit disposition](alpha-0.16-acceptance.md#dependency-audit-disposition).
+
+## 中文摘要
+
+0.16.0 Alpha 汇总自 0.15.2 以来的运行时与桌面更新:统一 Cooperative Chat,
+加入本地角色管理、启动恢复和实验性 Live2D,整理角色与连接设置,改进纹理驻留、
+语音调度、完整历史保存、配置声明及持久化可靠性。
+
+这是源码预发布,不包含桌面安装器、模型权重、角色素材、参考语音、Cubism Core
+或凭据。升级请使用新目录并保留完整的可选依赖配置;先停止输入和进行中的 Work。
+旧 Original Chat 开关已退役,角色切换需重启,声音与外观仍为应用级设置。
+已知限制和本轮实测范围见验收记录;既有单项测试不代表所有硬件与实验功能均已验收。
diff --git a/electron/package-lock.json b/electron/package-lock.json
index dad355f2..fa4a0b99 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "amadeus-desktop",
- "version": "0.15.2-alpha.0",
+ "version": "0.16.0-alpha.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "amadeus-desktop",
- "version": "0.15.2-alpha.0",
+ "version": "0.16.0-alpha.0",
"hasInstallScript": true,
"dependencies": {
"react": "^19.0.0",
@@ -18,7 +18,7 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^6.1.1",
- "concurrently": "^10.0.5",
+ "concurrently": "^10.0.6",
"electron": "^44.0.0",
"electron-builder": "^26.15.3",
"tailwindcss": "^4.0.0",
@@ -2179,15 +2179,15 @@
"license": "MIT"
},
"node_modules/concurrently": {
- "version": "10.0.5",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz",
- "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==",
+ "version": "10.0.6",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.6.tgz",
+ "integrity": "sha512-qgICXXFp2/3nAebRLmxcINKEXM1mLK3ij/y31P02VTs/KIkddIeC05n73DxOXsXMB9LFSqNU1/na3lDlOhcbIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "5.6.2",
"rxjs": "7.8.2",
- "shell-quote": "1.9.0",
+ "shell-quote": "1.12.0",
"supports-color": "10.2.2",
"tree-kill": "1.2.2",
"yargs": "18.0.0"
@@ -3546,9 +3546,9 @@
}
},
"node_modules/http-cache-semantics": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
- "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz",
+ "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==",
"dev": true,
"license": "BSD-2-Clause"
},
@@ -3744,9 +3744,9 @@
}
},
"node_modules/joi": {
- "version": "18.2.8",
- "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.8.tgz",
- "integrity": "sha512-G2TX62h58ZHuwqetJgP2F4ualakqAmZtBYe3jWen7gxQRw5xApX6crnFtuB91WC0c3ESBnva+kGSnb3+6pIQDQ==",
+ "version": "18.2.9",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.9.tgz",
+ "integrity": "sha512-2mD929bUVKUhOLQQEVhlf6EZ0Mlo0DeRb5MO7cViR9AXLtBauuccEtB1py9Ocxpo/P7ucnh442iY/iOwrh3IQw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -5744,9 +5744,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
- "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz",
+ "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/electron/package.json b/electron/package.json
index d730e3d8..6d2ce481 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -1,6 +1,6 @@
{
"name": "amadeus-desktop",
- "version": "0.15.2-alpha.0",
+ "version": "0.16.0-alpha.0",
"description": "Amadeus AI Companion — Electron frontend",
"type": "module",
"main": "dist/main/index.js",
@@ -37,7 +37,7 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^6.1.1",
- "concurrently": "^10.0.5",
+ "concurrently": "^10.0.6",
"electron": "^44.0.0",
"electron-builder": "^26.15.3",
"tailwindcss": "^4.0.0",
diff --git a/pyproject.toml b/pyproject.toml
index a597db93..f8db6d4f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "amadeus"
-version = "0.15.2a0"
+version = "0.16.0a0"
description = "Amadeus runtime: voice, chat runtime, provider host, and work ledger."
requires-python = ">=3.12,<3.13"
diff --git a/release/source_release_policy.json b/release/source_release_policy.json
index 44e51b6e..a785d08b 100644
--- a/release/source_release_policy.json
+++ b/release/source_release_policy.json
@@ -69,7 +69,11 @@
"docs/evidence/routing-accuracy-2026-09-12.json",
"docs/alpha-0.15-impact.md",
"docs/alpha-0.15.1.md",
- "docs/alpha-0.15.2.md"
+ "docs/alpha-0.15.2.md",
+ "docs/alpha-0.16.md",
+ "docs/alpha-0.16-acceptance.md",
+ "docs/chat-runtime-convergence.md",
+ "docs/live2d_character_visuals.md"
],
"include_roots": [
"characters",
@@ -181,6 +185,15 @@
"docs/provider_parent_conversation_handoff_2026-08-31.md",
"docs/alpha-0.15.1-acceptance.md",
"docs/alpha-0.15.2.md",
+ "docs/alpha-0.16.md",
+ "docs/alpha-0.16-acceptance.md",
+ "docs/chat-runtime-convergence.md",
+ "docs/live2d_character_visuals.md",
+ "docs/main-chat-character-prompt.md",
+ "docs/provider-handoff-context.md",
+ "docs/tts_adaptive_aggregation.md",
+ "docs/tts_emotion_references.md",
+ "docs/texture_cache_implementation_2026-10-05.md",
"docs/texture_pipeline_acceptance_2026-10-04.md",
"docs/evidence/texture-store-2026-10-04.json",
"docs/evidence/texture-egpu-comparison-2026-10-04.json",
diff --git a/tools/e2e_codex_app_server_control.py b/tools/e2e_codex_app_server_control.py
index db815c8b..04ea96b9 100644
--- a/tools/e2e_codex_app_server_control.py
+++ b/tools/e2e_codex_app_server_control.py
@@ -137,7 +137,8 @@ async def _run(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
async with WsProbe(f"ws://127.0.0.1:{port}/ws") as probe:
await probe.request(
"session.create",
- {"session_id": session_id, "title": "Codex active control probe"},
+ {"session_id": session_id, "title": "Codex active control probe",
+ "project_id": project_record.project_id},
)
create_turn, create_after, create_complete = await _send_chat(
probe,
@@ -146,7 +147,7 @@ async def _run(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
chat_provider=args.chat_provider,
timeout_s=args.chat_timeout,
text=(
- "切换到 control-lab 项目并交给 codex 做:先在项目根运行 "
+ "交给 codex 做:先在当前项目根运行 "
"python slow_gate.py,必须等它结束;然后创建 control.txt,"
"内容恰好是 ORIGINAL 加一个换行,并读取验证。"
),
@@ -198,15 +199,20 @@ async def _run(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
"仍然交给 codex,这是同一项工作的运行中修改。"
),
)
- steer = await probe.wait_event(
- lambda event: event.method == "provider.event"
- and event.params.get("run_id") == run_id
- and event.params.get("type") == "run.status"
- and event.params.get("payload", {}).get("stage") == "steer_queued",
- timeout=args.activity_timeout,
- after=amend_after,
- description="Codex native steer queued",
- )
+ # Work amendments use durable input delivery receipts. The retired
+ # steer_queued presentation event is not a delivery authority fact.
+ input_receipt = None
+ async with asyncio.timeout(args.activity_timeout):
+ while input_receipt is None:
+ with WorkLedgerStore(ledger_path) as store:
+ for item in store.list_work_items(limit=20):
+ for receipt in store.list_provider_inputs(item.work_item_id):
+ if receipt["input_id"] == amend_turn:
+ input_receipt = receipt
+ if input_receipt is None:
+ await asyncio.sleep(0.1)
+ if input_receipt["state"] != "delivered" or input_receipt["provider_run_id"] != run_id:
+ raise RuntimeError("Amendment was not delivered to the original Provider run")
amend_created_runs = [
event
for event in probe.state.events[amend_after:]
@@ -244,10 +250,8 @@ async def _run(args: argparse.Namespace) -> tuple[int, dict[str, Any]]:
"status_reply_is_visible": bool(
str(status_complete.params.get("full_text") or "").strip()
),
- "native_steer_was_queued": int(
- steer.params.get("payload", {}).get("revision") or 0
- )
- >= 1,
+ "amendment_delivered_to_same_run": input_receipt["state"] == "delivered"
+ and input_receipt["provider_run_id"] == run_id,
"amend_did_not_start_second_run": not amend_created_runs,
"one_work_item_one_attempt": len(ledger_items) == 1 and len(attempts) == 1,
"steered_result_is_current": content == "STEERED\n",
diff --git a/tools/e2e_codex_app_server_permission.py b/tools/e2e_codex_app_server_permission.py
index b373d30c..59ddf824 100644
--- a/tools/e2e_codex_app_server_permission.py
+++ b/tools/e2e_codex_app_server_permission.py
@@ -277,6 +277,7 @@ def is_next_control_event(event: Any) -> bool:
"permission_request_ids": permission_ids,
"native_permission_request_ids": sorted(native_request_ids),
"target_content": content,
+ "provider_result": terminal.to_dict(),
}
)
report["semantic_evidence"] = build_evidence(
@@ -297,7 +298,7 @@ def is_next_control_event(event: Any) -> bool:
"provider_run_ids": [run_id] if run_id else [],
},
manual_acceptance="pending",
- notes=f"Official Codex approval callback, Host {args.decision}, same native turn continuation.",
+ notes=[f"Official Codex approval callback, Host {args.decision}, same native turn continuation."],
)
exit_code = 0 if report["status"] == "passed" else 1
except Exception as exc:
diff --git a/tools/probes/measure_vn_audio_latency.py b/tools/probes/measure_vn_audio_latency.py
index b56b9a95..17ab516e 100644
--- a/tools/probes/measure_vn_audio_latency.py
+++ b/tools/probes/measure_vn_audio_latency.py
@@ -81,8 +81,6 @@ def log_event(record):
stage = "enqueue_ms"
elif "first audio chunk generated" in message:
stage = "audio_ready_ms"
- elif "first sound started" in message:
- stage = "first_sound_ms"
if stage:
current.setdefault(stage, round((record.created - current["started_at"]) * 1000))
changed.set()
@@ -94,16 +92,23 @@ def emit(self, record):
observer = Observer()
logging.getLogger().addHandler(observer)
logging.getLogger("PlaybackManager").setLevel(logging.INFO)
- original_write = player.write_audio_async
-
- async def observe_audio(audio, *positional, **kwargs):
+ # Both complete/cached playback and streaming reach PortAudio here. The
+ # async player method covers only streaming and is not device evidence.
+ import pyaudio
+ original_write = pyaudio.PyAudio.Stream.write
+
+ def observe_audio(stream, frames, *positional, **kwargs):
+ result = original_write(stream, frames, *positional, **kwargs)
+ if stream is not player.stream:
+ return result
import numpy as np
- array = audio.cpu().detach().numpy() if hasattr(audio, "detach") else np.asarray(audio)
+ array = np.frombuffer(frames, dtype=np.float32)
if array.size:
+ current.setdefault("first_device_write_ms", round((time.time() - current["started_at"]) * 1000))
current["peak"] = max(current.get("peak", 0), float(np.abs(array).max()))
current["samples"] = current.get("samples", 0) + int(array.size)
- await original_write(audio, *positional, **kwargs)
- player.write_audio_async = observe_audio
+ return result
+ pyaudio.PyAudio.Stream.write = observe_audio
async def speak(payload):
current.setdefault("speech_submitted_ms", round((time.time() - current["started_at"]) * 1000))
@@ -120,7 +125,7 @@ async def wait_audio(timeout=90):
while current.get("last_sentence_id") not in completed:
changed.clear()
await changed.wait()
- if not current.get("first_sound_ms") or not current.get("peak", 0) > 0:
+ if "first_device_write_ms" not in current or not current.get("peak", 0) > 0:
raise AssertionError("No nonzero device-playback evidence; silence placeholders do not pass")
workers = [asyncio.create_task(pipeline.play_sentence_worker()), asyncio.create_task(playback.run())]
@@ -184,6 +189,7 @@ async def whole_speak(header, text_complete=True):
task.cancel()
await asyncio.gather(*list(bridge._SUBTITLE_TASKS), return_exceptions=True)
player.cleanup()
+ pyaudio.PyAudio.Stream.write = original_write
synthesizer.backend.close()
executor.shutdown(wait=True)
logging.getLogger().removeHandler(observer)
diff --git a/uv.lock b/uv.lock
index 31cc6cec..99c47933 100644
--- a/uv.lock
+++ b/uv.lock
@@ -195,7 +195,7 @@ wheels = [
[[package]]
name = "amadeus"
-version = "0.15.2a0"
+version = "0.16.0a0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },