From dacc014feb4cc063c4f63f084bcc99a3080b8c17 Mon Sep 17 00:00:00 2001 From: tmoonlight Date: Fri, 2 Oct 2026 00:24:29 +0800 Subject: [PATCH 1/3] Improve session loading, tool result isolation and side chat support Include Windows Store packaging support and session loading design and verification records. Web tests, focused OpenSpec validation and MSIX packaging tests pass; release integration and final native validation follow. --- .github/workflows/refactor-matrix.yml | 15 +- CLAUDE.md | 18 +- docs/composer-tag-selection-testing.md | 7 + docs/daemon-api.md | 139 +++- docs/microsoft-store/privacy-policy.en-US.md | 70 ++ docs/microsoft-store/privacy-policy.zh-CN.md | 39 ++ docs/microsoft-store/submission.md | 93 +++ docs/microsoft-store/verification.md | 64 ++ installer/windows/store/.gitignore | 1 + installer/windows/store/README.md | 30 + installer/windows/store/build_msix.py | 276 ++++++++ .../changes/add-floating-side-chat/design.md | 4 +- .../specs/floating-side-chat/spec.md | 12 + .../fix-reused-tool-call-ids/.openspec.yaml | 2 + .../fix-reused-tool-call-ids/design.md | 28 + .../fix-reused-tool-call-ids/proposal.md | 24 + .../specs/provider-tool-call-identity/spec.md | 37 + .../changes/fix-reused-tool-call-ids/tasks.md | 31 + .../.openspec.yaml | 2 + .../design.md | 26 + .../proposal.md | 24 + .../specs/tool-result-storage/spec.md | 53 ++ .../tasks.md | 23 + .../optimize-session-loading/.openspec.yaml | 2 + .../optimize-session-loading/design.md | 207 ++++++ .../optimize-session-loading/proposal.md | 53 ++ .../specs/session-history-loading/spec.md | 96 +++ .../specs/session-load-diagnostics/spec.md | 39 ++ .../specs/session-resume/spec.md | 32 + .../specs/sidebar-session-collapse/spec.md | 23 + .../sidebar-session-list-loading/spec.md | 53 ++ .../changes/optimize-session-loading/tasks.md | 91 +++ .../verification/README.md | 37 + .../verification/baseline-browser.json | 578 +++++++++++++++ .../verification/baseline-http.json | 79 +++ .../verification/baseline-steady-browser.json | 657 ++++++++++++++++++ .../verification/baseline.md | 19 + .../verification/final-validation.json | 20 + .../verification/final-validation.md | 29 + .../verification/stage0-single-load.md | 12 + .../verification/stage1-parsing-http.json | 79 +++ .../verification/stage1-sidebar-browser.json | 557 +++++++++++++++ .../verification/stage1-sidebar.md | 9 + .../stage1-single-load-browser.json | 539 ++++++++++++++ .../verification/stage1-storage.md | 19 + .../verification/stage1-unlocked-http.json | 79 +++ .../verification/stage2-browser-100.json | 565 +++++++++++++++ .../verification/stage2-browser-20.json | 558 +++++++++++++++ .../verification/stage2-http.json | 80 +++ .../verification/stage2-navigation.json | 34 + .../verification/stage2-results.md | 37 + .../.openspec.yaml | 2 + .../publish-microsoft-store-msix/design.md | 40 ++ .../publish-microsoft-store-msix/proposal.md | 25 + .../specs/microsoft-store-package/spec.md | 48 ++ .../specs/self-upgrade/spec.md | 16 + .../publish-microsoft-store-msix/tasks.md | 21 + .../tasks.md | 2 +- .../verification/P4-04-multi-platform.md | 88 +++ .../bench/session_loading/check_navigation.py | 68 ++ scripts/bench/session_loading/generate.py | 144 ++++ scripts/bench/session_loading/inspect_data.py | 34 + scripts/bench/session_loading/measure.py | 198 ++++++ src/adapters/feedback/feedback_upload.cpp | 22 +- src/adapters/feedback/feedback_upload.hpp | 4 + src/adapters/tool/file_state_restore.cpp | 121 +++- src/adapters/tool/file_state_restore.hpp | 4 +- src/adapters/tool/mtime_tracker.cpp | 17 + src/adapters/tool/mtime_tracker.hpp | 16 + src/adapters/upgrade/check.hpp | 1 + src/adapters/upgrade/upgrade.cpp | 12 + src/apps/cli/command_dispatch.cpp | 8 + src/apps/cli/diagnose_sessions.cpp | 33 + src/apps/cli/diagnose_sessions.hpp | 6 + src/apps/cli/process_environment.cpp | 9 + src/apps/desktop/context_items.cpp | 34 +- src/apps/desktop/context_items.hpp | 5 +- src/apps/tui/commands/builtin_commands.cpp | 6 +- .../web/handlers/pinned_sessions_handler.hpp | 14 + .../web/handlers/session_history_handler.hpp | 55 ++ .../web/handlers/session_list_handler.hpp | 4 + src/apps/web/routes/routes_diagnostics.cpp | 64 ++ src/apps/web/routes/routes_sessions.cpp | 92 ++- src/apps/web/routes/routes_workspaces.cpp | 14 +- src/apps/web/routes/routes_ws.cpp | 26 +- src/apps/web/server.cpp | 1 + src/apps/web/server_helpers.cpp | 95 +-- src/apps/web/server_impl.hpp | 10 +- src/base/platform/package_identity.cpp | 23 + src/base/platform/package_identity.hpp | 9 + src/base/utils/atomic_file.hpp | 14 +- src/domain/session/compact_checkpoint.cpp | 4 +- src/domain/session/event_dispatcher.cpp | 4 +- src/domain/session/event_dispatcher.hpp | 3 +- src/domain/session/global_session_catalog.cpp | 64 +- src/domain/session/global_session_catalog.hpp | 7 +- .../session/global_session_catalog_meta.hpp | 34 + src/domain/session/global_session_search.cpp | 4 + src/domain/session/global_session_search.hpp | 1 + src/domain/session/jsonl_scanner.cpp | 70 ++ src/domain/session/jsonl_scanner.hpp | 32 + src/domain/session/session_client.hpp | 10 + .../session/session_data_diagnostics.cpp | 118 ++++ .../session/session_data_diagnostics.hpp | 19 + src/domain/session/session_file_reader.cpp | 91 +++ src/domain/session/session_file_reader.hpp | 44 ++ src/domain/session/session_history_page.cpp | 252 +++++++ src/domain/session/session_history_page.hpp | 61 ++ .../session/session_history_recovery.cpp | 54 +- .../session/session_history_recovery.hpp | 1 + src/domain/session/session_load_metrics.hpp | 88 +++ src/domain/session/session_manager.cpp | 104 ++- src/domain/session/session_manager.hpp | 22 +- .../session/session_manager_history.cpp | 116 ++++ src/domain/session/session_pin_store.cpp | 20 +- src/domain/session/session_serializer.cpp | 14 +- src/domain/session/session_serializer.hpp | 3 + src/domain/session/session_storage.cpp | 43 +- src/domain/session/session_storage.hpp | 6 + .../session/session_user_message_search.cpp | 87 ++- .../session/session_user_message_search.hpp | 3 + src/domain/session/tool_result_storage.cpp | 13 +- src/engine/agent/agent_loop.hpp | 6 +- .../agent/compaction/compaction_window.cpp | 5 +- .../agent/event_payload/message_payload.cpp | 7 +- .../side_question/agent_side_question.cpp | 17 +- src/engine/agent/side_question/side_chat.cpp | 313 +++++++-- src/engine/agent/side_question/side_chat.hpp | 44 +- .../agent/side_question/side_chat_tools.cpp | 108 +++ .../agent/side_question/side_chat_tools.hpp | 27 + .../side_question/side_question_service.cpp | 134 ++-- .../side_question/side_question_service.hpp | 21 +- .../agent/transcript/agent_transcript.cpp | 11 +- .../agent/transcript/conversation_history.cpp | 35 +- .../agent/transcript/conversation_history.hpp | 6 + src/engine/agent/turn/turn_runner.cpp | 4 + src/engine/agent/turn/user_turn_message.cpp | 12 - src/engine/agent/turn/user_turn_message.hpp | 2 - .../session_host/local_session_client.cpp | 13 +- .../session_host/local_session_client.hpp | 5 + src/host/session_host/session_registry.cpp | 44 +- src/host/session_host/session_registry.hpp | 4 +- .../agent_loop_tool_result_storage_test.cpp | 82 ++- .../event_payload/message_payload_test.cpp | 18 + tests/agent/side_chat_test.cpp | 254 ++++++- .../side_question/side_chat_tools_test.cpp | 162 +++++ .../side_question_service_test.cpp | 53 +- .../transcript/conversation_history_test.cpp | 57 ++ tests/agent/turn/user_turn_message_test.cpp | 8 - tests/desktop/context_items_test.cpp | 40 +- tests/feedback/feedback_upload_test.cpp | 15 + tests/provider/anthropic_provider_test.cpp | 34 + .../openai_provider_reasoning_test.cpp | 43 ++ tests/provider/tool_image_feedback_test.cpp | 62 +- tests/scripts/build_msix_test.py | 78 +++ tests/session/event_dispatcher_test.cpp | 14 + tests/session/global_session_catalog_test.cpp | 51 ++ .../session/session_data_diagnostics_test.cpp | 66 ++ tests/session/session_file_reader_test.cpp | 25 + tests/session/session_history_page_test.cpp | 185 +++++ .../session/session_history_recovery_test.cpp | 152 ++++ tests/session/session_load_metrics_test.cpp | 41 ++ tests/session/session_manager_rewind_test.cpp | 36 + tests/session/session_serializer_test.cpp | 15 + .../session_user_message_search_test.cpp | 67 ++ tests/session/tool_result_storage_test.cpp | 51 +- .../session_history_concurrency_test.cpp | 76 ++ tests/session_host/session_registry_test.cpp | 153 +++- tests/tool/file_state_restore_test.cpp | 48 ++ tests/tool/mtime_tracker_test.cpp | 39 ++ tests/tui/commands/builtin_commands_test.cpp | 28 +- tests/web/pinned_sessions_handler_test.cpp | 10 + tests/web/session_list_handler_test.cpp | 7 + tests/web/web_server_smoke_test.cpp | 223 +++++- web/scripts/i18n-en-overrides.mjs | 7 + web/src/App.jsx | 11 +- web/src/components/ChatView.jsx | 98 ++- web/src/components/InputBar.jsx | 4 +- web/src/components/Message.jsx | 25 +- web/src/components/SideChatWindow.jsx | 42 +- web/src/components/Sidebar.jsx | 81 +-- web/src/components/UpdateDialog.jsx | 33 +- web/src/i18n/sourceCatalog.generated.js | 14 + web/src/lib/api.js | 15 +- .../chatFileDropFeedbackArchitecture.test.js | 5 +- web/src/lib/chatScrollFollow.js | 1 + web/src/lib/chatScrollFollow.test.js | 13 + web/src/lib/composerFileIntake.js | 51 +- web/src/lib/composerFileIntake.test.js | 74 +- web/src/lib/composerImagePresentation.js | 9 +- web/src/lib/composerMessageRendering.test.js | 83 ++- web/src/lib/connection.js | 20 +- .../lib/connectionStatusSubscriptions.test.js | 47 ++ web/src/lib/navigationHistory.js | 5 + web/src/lib/navigationHistory.test.js | 7 + web/src/lib/runTests.js | 8 + web/src/lib/sessionHistoryPaging.test.js | 33 + web/src/lib/sessionJump.js | 22 +- web/src/lib/sessionJump.test.js | 12 + web/src/lib/sessionOpenDiagnostics.js | 50 ++ web/src/lib/sessionOpenDiagnostics.test.js | 25 + web/src/lib/sessionTranscript.js | 191 ++++- web/src/lib/sessionTranscript.test.js | 6 +- web/src/lib/sessionTranscriptLoading.test.js | 99 +++ web/src/lib/sessionTranscriptRecovery.test.js | 72 +- web/src/lib/sideChatController.js | 79 ++- web/src/lib/sideChatController.test.js | 57 +- web/src/lib/sideChatStream.js | 21 +- web/src/lib/sideChatStream.test.js | 33 + ...idebarWorkspaceHeadingArchitecture.test.js | 2 +- web/src/lib/sidebarWorkspaceRefresh.test.js | 60 +- web/src/lib/sidebarWorkspaceSessions.js | 7 +- web/src/lib/sidebarWorkspaceSessions.test.js | 6 +- web/src/lib/transcriptItemPresentation.js | 1 + web/src/lib/updateJob.js | 1 + web/src/lib/updateJob.test.js | 13 + web/src/styles/side-chat.css | 57 ++ 217 files changed, 12153 insertions(+), 756 deletions(-) create mode 100644 docs/microsoft-store/privacy-policy.en-US.md create mode 100644 docs/microsoft-store/privacy-policy.zh-CN.md create mode 100644 docs/microsoft-store/submission.md create mode 100644 docs/microsoft-store/verification.md create mode 100644 installer/windows/store/.gitignore create mode 100644 installer/windows/store/README.md create mode 100644 installer/windows/store/build_msix.py create mode 100644 openspec/changes/fix-reused-tool-call-ids/.openspec.yaml create mode 100644 openspec/changes/fix-reused-tool-call-ids/design.md create mode 100644 openspec/changes/fix-reused-tool-call-ids/proposal.md create mode 100644 openspec/changes/fix-reused-tool-call-ids/specs/provider-tool-call-identity/spec.md create mode 100644 openspec/changes/fix-reused-tool-call-ids/tasks.md create mode 100644 openspec/changes/fix-tool-result-identity-collisions/.openspec.yaml create mode 100644 openspec/changes/fix-tool-result-identity-collisions/design.md create mode 100644 openspec/changes/fix-tool-result-identity-collisions/proposal.md create mode 100644 openspec/changes/fix-tool-result-identity-collisions/specs/tool-result-storage/spec.md create mode 100644 openspec/changes/fix-tool-result-identity-collisions/tasks.md create mode 100644 openspec/changes/optimize-session-loading/.openspec.yaml create mode 100644 openspec/changes/optimize-session-loading/design.md create mode 100644 openspec/changes/optimize-session-loading/proposal.md create mode 100644 openspec/changes/optimize-session-loading/specs/session-history-loading/spec.md create mode 100644 openspec/changes/optimize-session-loading/specs/session-load-diagnostics/spec.md create mode 100644 openspec/changes/optimize-session-loading/specs/session-resume/spec.md create mode 100644 openspec/changes/optimize-session-loading/specs/sidebar-session-collapse/spec.md create mode 100644 openspec/changes/optimize-session-loading/specs/sidebar-session-list-loading/spec.md create mode 100644 openspec/changes/optimize-session-loading/tasks.md create mode 100644 openspec/changes/optimize-session-loading/verification/README.md create mode 100644 openspec/changes/optimize-session-loading/verification/baseline-browser.json create mode 100644 openspec/changes/optimize-session-loading/verification/baseline-http.json create mode 100644 openspec/changes/optimize-session-loading/verification/baseline-steady-browser.json create mode 100644 openspec/changes/optimize-session-loading/verification/baseline.md create mode 100644 openspec/changes/optimize-session-loading/verification/final-validation.json create mode 100644 openspec/changes/optimize-session-loading/verification/final-validation.md create mode 100644 openspec/changes/optimize-session-loading/verification/stage0-single-load.md create mode 100644 openspec/changes/optimize-session-loading/verification/stage1-parsing-http.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage1-sidebar-browser.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage1-sidebar.md create mode 100644 openspec/changes/optimize-session-loading/verification/stage1-single-load-browser.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage1-storage.md create mode 100644 openspec/changes/optimize-session-loading/verification/stage1-unlocked-http.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage2-browser-100.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage2-browser-20.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage2-http.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage2-navigation.json create mode 100644 openspec/changes/optimize-session-loading/verification/stage2-results.md create mode 100644 openspec/changes/publish-microsoft-store-msix/.openspec.yaml create mode 100644 openspec/changes/publish-microsoft-store-msix/design.md create mode 100644 openspec/changes/publish-microsoft-store-msix/proposal.md create mode 100644 openspec/changes/publish-microsoft-store-msix/specs/microsoft-store-package/spec.md create mode 100644 openspec/changes/publish-microsoft-store-msix/specs/self-upgrade/spec.md create mode 100644 openspec/changes/publish-microsoft-store-msix/tasks.md create mode 100644 scripts/bench/session_loading/check_navigation.py create mode 100644 scripts/bench/session_loading/generate.py create mode 100644 scripts/bench/session_loading/inspect_data.py create mode 100644 scripts/bench/session_loading/measure.py create mode 100644 src/apps/cli/diagnose_sessions.cpp create mode 100644 src/apps/cli/diagnose_sessions.hpp create mode 100644 src/apps/web/handlers/session_history_handler.hpp create mode 100644 src/apps/web/routes/routes_diagnostics.cpp create mode 100644 src/base/platform/package_identity.cpp create mode 100644 src/base/platform/package_identity.hpp create mode 100644 src/domain/session/global_session_catalog_meta.hpp create mode 100644 src/domain/session/jsonl_scanner.cpp create mode 100644 src/domain/session/jsonl_scanner.hpp create mode 100644 src/domain/session/session_data_diagnostics.cpp create mode 100644 src/domain/session/session_data_diagnostics.hpp create mode 100644 src/domain/session/session_file_reader.cpp create mode 100644 src/domain/session/session_file_reader.hpp create mode 100644 src/domain/session/session_history_page.cpp create mode 100644 src/domain/session/session_history_page.hpp create mode 100644 src/domain/session/session_load_metrics.hpp create mode 100644 src/domain/session/session_manager_history.cpp create mode 100644 src/engine/agent/side_question/side_chat_tools.cpp create mode 100644 src/engine/agent/side_question/side_chat_tools.hpp create mode 100644 tests/agent/side_question/side_chat_tools_test.cpp create mode 100644 tests/scripts/build_msix_test.py create mode 100644 tests/session/session_data_diagnostics_test.cpp create mode 100644 tests/session/session_file_reader_test.cpp create mode 100644 tests/session/session_history_page_test.cpp create mode 100644 tests/session/session_load_metrics_test.cpp create mode 100644 tests/session_host/session_history_concurrency_test.cpp create mode 100644 tests/tool/file_state_restore_test.cpp create mode 100644 web/src/lib/connectionStatusSubscriptions.test.js create mode 100644 web/src/lib/sessionHistoryPaging.test.js create mode 100644 web/src/lib/sessionOpenDiagnostics.js create mode 100644 web/src/lib/sessionOpenDiagnostics.test.js create mode 100644 web/src/lib/sessionTranscriptLoading.test.js diff --git a/.github/workflows/refactor-matrix.yml b/.github/workflows/refactor-matrix.yml index 7f5ea26a9..495f2bfe1 100644 --- a/.github/workflows/refactor-matrix.yml +++ b/.github/workflows/refactor-matrix.yml @@ -305,6 +305,19 @@ jobs: libegl1-mesa-dev libudev-dev libxi-dev libcups2-dev librsvg2-dev rm -rf /var/lib/apt/lists/* + - name: Record verified source revision + shell: bash + run: | + # Buster's Git security backport ignores command-scope safe.directory. + # Trust only this ephemeral checkout, then fail if HEAD is unavailable. + git config --global --add safe.directory "$GITHUB_WORKSPACE" + source_sha="$(git -C "$GITHUB_WORKSPACE" rev-parse --verify HEAD)" + if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "Invalid source revision: $source_sha" >&2 + exit 1 + fi + printf 'SOURCE_REVISION=%s\n' "$source_sha" >> "$GITHUB_ENV" + - name: Set up CMake uses: jwlawson/actions-setup-cmake@v2 with: @@ -366,7 +379,7 @@ jobs: "runner_image": "ubuntu-22.04 / buildpack-deps:buster", "label": "${{ inputs.label }}", "source_ref_input": "${{ inputs.source_ref }}", - "source_revision": "$(git -c safe.directory='*' rev-parse HEAD)", + "source_revision": "${SOURCE_REVISION}", "tools_revision": "${{ github.sha }}", "workflow_run_id": "${{ github.run_id }}", "run_tests": false, diff --git a/CLAUDE.md b/CLAUDE.md index 9ed603f06..b42a38b8d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,6 +156,8 @@ generates one standard-quality image and must never run on load or save. Core tools are registered for both TUI and daemon paths: `bash`, `file_read`, `file_write`, `file_edit`, `grep`, `glob`, `task_complete`, `AskUserQuestion`, skill tools, memory tools, optional `web_search`, and MCP tools. `ToolResult` can carry summaries and hunks so TUI/web resume can render useful compact rows instead of raw output folds. +**侧边对话只放只读工具(side chat read-only tools)。** Web/Desktop 浮动侧边对话曾不带工具表,模型照着主会话历史把 `bash` 调用写成尖括号正文显示出来。现在 `run_side_chat`(`src/engine/agent/side_question/side_chat.cpp`)是一个小工具循环:工具集由 `side_chat_tools.cpp::build_side_chat_toolset` 组装,白名单 `file_read` / `grep` / `glob` / `lsp` 与会话专家策略取交集、MCP 一律不放(**不要**改成按 `is_read_only` 筛 —— spawn_subagent / AskUserQuestion / goal 工具也带这个免确认标记);每次调用过 Deny 规则 → 主会话同款 `PathAccessPolicy` → 危险路径,凡是要弹确认的一律拒绝(侧边对话没有确认通道),并在 `MtimeTracker::DetachedReadScope` 里执行 —— 否则侧边读过的文件,主代理再读只拿到「未变化」短桩,或不读就能编辑。最多 8 轮工具;正文里的文本工具调用丢弃并纠正(最多 2 次),绝不显示。协议:`side_chat_tool` 帧报工具进度,`side_chat_reset` 只丢弃当前这一步的正文(工具之前的正文保留),前端 `sideChatTurnParts` 按工具发生位置把回答切成「正文 / 工具行」。TUI `/btw` / `/side` 与同步 HTTP `/side-question` 走同一个循环(`SideQuestionService::run_question`,无追问历史、无流式回调):结果的 `tools_used` 由 TUI 经 `side_question_tools_note` 显示在回答前,HTTP 原样返回;Codex 原生 provider 自带工具运行时,一律拒绝。`stop_requests` 会取消在途的旁路请求(含 Web 流式),关会话不用等完整个工具循环。回归:`tests/agent/side_chat_test.cpp`、`tests/agent/side_question/side_chat_tools_test.cpp`、`side_question_service_test.cpp::StopRequestsCancelsInFlightQuestion`、`session_registry_test.cpp::SideQuestionReadsFilesWithReadOnlyToolsOnly`、`mtime_tracker_test.cpp::DetachedReadScope*`、前端 `sideChatController` / `sideChatStream` 测试。 + `spawn_subagent` / `wait_subagent` are registered in both daemon (`worker.cpp`) and TUI (via `src/apps/tui/subagent_host.{hpp,cpp}` — SessionRegistry/LocalSessionClient have no web dependency, so the TUI process instantiates them directly; the TUI main session lives outside the registry, so its permission mode reaches children through `SubagentToolDeps::fallback_permissions`, and `on_spawn` lets the host register tasks + subscribe child events): a sub-agent is a normal SessionRegistry session (isolated context) created with the parent's cwd and permission mode. `spawn_subagent(prompt, wait=true)` blocks until the child turn finishes and returns its final assistant reply into the parent context; `wait=false` is fire-and-forget for pipeline handoff, joined later via `wait_subagent(session_id)`. Prompts starting with `/` go through the same skill-command expansion as Web input. Sub-agents cannot spawn further sub-agents (`SessionEntry::subagent_depth`). Implementation: [src/host/session_host/tools/spawn_subagent_tool.cpp](src/host/session_host/tools/spawn_subagent_tool.cpp); deps are late-bound via shared_ptr because ToolExecutor is constructed before SessionRegistry in worker.cpp. **TUI surface**: the right sidebar's "Background Tasks" section lists running sub-agents only (● title + elapsed; removed the moment the child turn ends — user decision); `/tasks [list|abort |clear]` is the operation entry (clear = same permanent-purge semantics as Web, via `SessionStorage::purge_session_files`). A child's `permission_request` is queued (`TuiState::remote_confirm_queue`) and pumped into the confirm overlay when free (origin-labelled; the answer routes back via `SubagentHost::respond_permission`). AskUserQuestion needs no bridging — children share the TUI ToolExecutor, so the TUI ask tool runs directly; it now queues on `TuiState::overlay_cv` until the confirm/ask overlay is free and sets `ask_origin_label` when the caller is a sub-agent. `on_tool_confirm` in tui/app/tui_agent_bridge.cpp queues on the same cv, so concurrent overlay claims (main confirm / child ask / remote pump) serialize instead of clobbering each other. @@ -257,12 +259,12 @@ Rewind support uses per-user-turn checkpoints. `SessionManager::track_file_write - **`accept` 拒掉的条目不占 limit 名额**,否则一串归档会话就能把整页挤空,侧边栏会显示成「这个 workspace 没有会话」。 - **`sessions_for_workspace` 里活跃会话逐个点读自己那一个 meta**(`read_meta(meta_path(dir, id))`),不要为了给它们配 meta 而把整个目录读一遍 —— 那正是这条路径原来 100% 的耗时来源。行先攒成 (updated_at, id, body) 再统一排序截断:旧实现按「active 一段 + disk 一段」的拼接顺序截断,取出来的前 N 条并不是最新的 N 条。 - **那个 `dir` 必须由会话自己的 cwd 推导,不能用调用方 workspace 的 cwd。** 会话 meta 的落盘目录恒为 `get_project_dir(该会话的 cwd)`(`SessionManager::ensure_created` 就是这么定 `project_dir_` 的),而**一个 daemon 经 routes_workspaces 服务多个 workspace** —— 侧栏切项目不换进程,`compatibility_workspace()` 始终是进程 cwd。拿进程 cwd 去读别的 workspace 的会话 meta 只会读到空,而 `SessionRegistry::list_active()` 出于热路径考虑本来就不填 `created_at`/`updated_at`(靠这次点读补),于是这两个字段静默变成空串。实测症状:Web「后台任务」面板卡片耗时**永远停在 00s**(前端 `taskElapsedSeconds` 在 `createdAtMs` 为 0 时直接 return 0,每秒 tick 也推不动),`GET /api/sessions?parent=` 整个返回 `[]` 而同一时刻 `GET /api/workspaces//sessions?parent=` 返回完整两条。同理,后台任务查询的**磁盘枚举**要用父会话 cwd 的 project_dir,否则已结束的子任务在面板里整个消失。这是 junction 教训(见 LSP 一节)的同族问题 —— 凡是拿路径当 key 的地方,两侧形态必须同源。回归测试:`web_server_smoke_test.cpp::SubagentQueryReadsMetaFromSessionOwnWorkspace`。注意该用例必须给子会话补一条消息才有意义 —— meta 是 lazy 落盘的,只 create 不发消息时磁盘上根本没有 meta,测到的就不是「读错目录」。 -- **截断后 `total` 只是上界**(目录里的候选文件数),精确总数拿不到。REST 因此多回 `total_exact` 与 `has_more`,前端判断「是否已全量加载」只能看 `has_more`;拿 `sessions.length >= total` 比会在上界下判错,展开时该补的全量请求就被跳过了。 +- **截断后 `total` 只是上界**(目录里的候选文件数),精确总数拿不到。REST 因此多回 `total_exact` 与 `has_more`,前端判断「是否已全量加载」只能看 `has_more`;拿 `sessions.length >= total` 比会在上界下判错,展开时该补的有界请求就被跳过了。 - 文件名判定(`is_canonical_meta_filename`)是手写字符扫描而不是 `std::regex` —— 上千条目的目录里每项跑两次正则的开销已经能量到。改它要同时守住:PID 后缀的旧实验数据必须排除,headless `--session-id` 的自定义 id 字符集必须接受。 回归测试:[tests/session/session_metadata_page_test.cpp](tests/session/session_metadata_page_test.cpp)(其中 `BoundedPageStopsReadingOnceItHasEnough` 的 `accepted <= 13` 就是「没退化成读全部再截断」的哨兵)+ `tests/web/session_list_handler_test.cpp` + `web_server_smoke_test.cpp::WorkspaceSessionListLimitReturnsEnvelope`。 -**仍未分页的两条全量路径**(已知,尚未优化):`GET /api/sessions`(`include_no_workspace=true`,要并 no-workspace 缓存目录)与 WS 连接建立时的 `send_status_snapshot()` —— 后者每次建连都对当前 workspace 全量枚举一次。 +**旧全量列表继续兼容。** 侧栏使用 `GET /api/sessions?scope=no-workspace`,展开工作区按当前可见行数加 5 再加置顶数请求,周期刷新保持该界限;父会话查询只读父会话的工作区并默认限制 100 条。WS 首次状态订阅仍需要工作区快照,客户端和服务端均去重,重连后才重新订阅。 **侧边栏卡顿的第二个来源:请求条数,不是单个请求的耗时。** 侧边栏有个 5 秒的 `setInterval(refresh)`,而 `refresh()` 里对**全部** workspace 各扇出一次 `pinned-sessions`(还是 `await Promise.all`,阻塞后续)和一次 `opencode-import`。14 个 workspace 就是 28 个请求/轮,浏览器对同一域名只有 6 条并发连接 —— 实测页面开着 30 秒发了 **220 个 API 请求**,`pinned-sessions` 84 次累计 8974ms、`opencode-import` 84 次累计 7660ms,而**每个请求的 `responseStart - startTime` 几乎等于它的总耗时**,即 400ms+ 全是排队,服务端处理只要十几毫秒。用户点击展开时那个几毫秒的会话列表请求排在一百多个请求后面,表现就是侧边栏长时间停在「加载中...」。 @@ -272,7 +274,7 @@ Rewind support uses per-user-turn checkpoints. `SessionManager::track_file_write - **`pinned-sessions` 只对可见 workspace 重取**,折叠的沿用 `pinnedByWorkspaceRef` 缓存值 —— 注意是「沿用」不是「清空」,否则展开时置顶标记会闪一下。 - **`opencode-import` 首轮全探、之后只重探可见的**。它探的是「这个目录有没有 opencode 数据可导入」,近乎静态,5 秒一次对全部 workspace 重探纯属浪费;新出现的 workspace 仍会补探一次,导入提示不会丢。 -- **`session_ids_for_workspace` 曾用 `list_sessions()`** —— 它只需要 id 与 archived,却让 `enrich_meta_from_messages` 逐个打开 JSONL(单个 workspace 实测 523MB),而且把同一个目录扫了两遍。改用 `list_session_metadata()` 后单次 `pinned-sessions` 从 9~16ms 降到 5.7ms(冷缓存差距更大)。 +- **`session_ids_for_workspace` 曾用 `list_sessions()`** —— 它只需要 id 与 archived,却让 `enrich_meta_from_messages` 逐个打开 JSONL(单个 workspace 实测 523MB),而且把同一个目录扫了两遍。现在只对置顶 ID 点读对应 meta;无置顶时零元数据读取。侧栏有界会话页先显示,置顶标记与排序随后合并。 **`WorkspaceRegistry::scan()` 的负缓存。** `~/.acecode/projects` 下每个用过的 cwd 都留一个 hash 目录,实测 **16568 个**,而带 `workspace.json` 的只有 34 个。拆开测:枚举全部目录连 mtime 只要 **12.8ms**,逐个探 `workspace.json` 却要 **267ms** —— 95% 的时间花在对一万六千个目录做 `fs::exists`。而 `/api/workspaces` 与 `/api/pinned-sessions/order` 每轮 refresh 各调一次 scan。 @@ -475,7 +477,7 @@ writes finish and refills the password from the authenticated settings response. 5. **AsyncPrompter waiters** — AgentLoop blocks on a per-session condvar; unblock posted from the Crow handler thread processing `decision` 6. **Attention flusher** — `WebServer::Impl::start_attention_flusher`,每 `kAttentionFlushIntervalMs`(1000)把脏 workspace 的 session 未读态落盘 -**`app_config_mu` 是 shared_mutex;只读路径共享、写路径独占。** 两条 web resume 路由使用 `shared_lock`:resume 对 config 只读但要全量解析 jsonl(实测 574~824ms/次),独占持有会把整个 HTTP 面卡停近一秒。其他只读取或快照 config 的路由也必须使用 `shared_lock`,这样恢复会话时仍可并发处理模型列表、健康状态等读取;settings 变更、saved_models 落盘、`refresh_default_session_preferences` 等写方必须使用 `lock_guard` 独占。同 id 并发 resume 由 `SessionRegistry::resume` 入口的单飞守卫(`resume_inflight_`)串行化,后到者等首个完成后命中 `entries_` 快速路径;没有这层,两个 `make_entry` 并发会互抢 writer lease,输家析构时还可能清掉赢家的 lease。 +**`app_config_mu` 是 shared_mutex;只读快照共享、写路径独占。** Web 恢复入口使用会话注册表持有的依赖与配置快照,不能把全量 IO 包在配置锁内;两条恢复路由已不再直接持有旧的 `shared_lock`。只读取/复制 config 的路由在复制期间用共享锁,settings、saved_models 与默认偏好刷新等写路径用独占锁。同 id 并发恢复仍由 `SessionRegistry::resume` 的 `resume_inflight_` 单飞守卫串行化,防止重复 entry 与 writer lease 竞争。 **Attention 落盘是节流的,别改回逐事件写。** `note_session_event_for_attention` 由 WS 订阅的 listener 调用,而 `EventDispatcher::emit` 是在**发射线程(AgentLoop worker)上同步 drain 订阅者**的 —— 所以这个函数跑在 agent 线程上,且 Token / Reasoning / Tool* 事件都会推进 `update_cursor`。曾经每个这样的事件都整份重写 workspace 的 attention 文件(tmp + rename),实测流式峰值约 500 事件/秒(feedback IQSZ-D0668:相邻日志行 lastSeq 差 45 / 92ms),多会话并发时还是同一个文件,把磁盘和 `attention_mu` 一起打满。现在热路径只 `attention_dirty_workspaces.insert`,由上面那条 flusher 线程合并写;只有状态跃迁(read↔unread↔in_progress / busy 翻转,即回合边界)才同步落盘。不变量:成功写出才清掉对应脏标记,失败保留到下个周期重试;析构时先停止事件 producer,最后停 flusher 并落盘剩余状态。 @@ -558,6 +560,10 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `