sync: taiji 稳定版同步最新基线 aa982617a(群聊交付 + CI 治理 + 上游合流) - #2232
Merged
GCWing merged 48 commits intoAug 12, 2026
Conversation
Remote scenarios routinely put two different BitFun versions on one connection, and users upgrade in place, but the agent guide had no rule covering either. Add an "Upgrade compatibility" global rule next to the remote scenarios section, matching what the existing remote-workspace and transport designs already practice: - persisted shapes stay tolerant, defaulted, and never repurposed; - unparseable data degrades instead of being deleted or reset; - cross-version boundaries advertise and check a capability rather than assuming behavior from a package version; - a rename is a migration, including the data it references; - upgrade coverage means legacy deserialization and old-payload round trips, not only the current shape. AGENTS-CN.md is updated to match.
The Web UI transport adapter denies 10 controller-owned commands that neither peer host refuses: the nine `speech_*` capture/model commands and `dispatch_continue`. The controller-side list is only an optimization — an older controller build, or any non-Web-UI controller, still reaches a peer host over HostInvoke, and the host would then run them. That means microphone capture sessions and speech model file management executing on the peer instead of the machine the user speaks at, and a dispatch control-plane verb running against the peer's outbound observer records and SSH credentials rather than the controller's. Every sibling dispatch verb is already denied on all three lists; `dispatch_continue` was simply missed. Deny both families on the desktop and CLI peer hosts, and add contract tests covering them. The README already required these three lists to stay aligned, but nothing enforced it. Add a peer command policy check to the core boundary checker that fails when a controller-denied command is missing from either host list, so the boundary cannot drift again. The check is one-way: a host denying more than the controller stays allowed. The CLI's four pre-handled control-plane commands are declared exceptions, and a stale exception is itself reported. Verified: cargo test -p bitfun-cli --bins peer_host, cargo test -p bitfun-desktop --lib peer_host_invoke / remote_workspace_policy, and the new check both passing on the fix and reporting all 10 commands without it.
Reconnecting to a peer device left the chat blank until the user clicked a session in the sidebar. Three gaps in `initializeWorkspace`, all reachable only in Peer Device Mode because `clearAllSessionsForPeerSwitch` is the one place that bumps the store surface generation: 1. A metadata page loaded across a surface-generation bump is discarded by `processPersistedSessionMetadataList`, but the page is still returned with its sessions. The caller then sees "history exists" over an empty store. Reload once against the settled generation. 2. History was only restored on the auto-select path, so a session that was already active but still metadata-only was never hydrated. The breadcrumb and turn rail render from the catalog while the message area stays empty — the reported symptom. Restore history for an active historical session too. 3. When metadata claimed sessions but none were selectable, initialize returned true while selecting nothing. The caller reads true as "do not create a session", so the surface ended up with neither. Report no history so the caller creates against the live workspace instead. No persisted shape, command, or protocol changes; an older peer host is unaffected. Verified: all three tests fail without the fix and pass with it; the 8 existing FlowChatManager cases are unchanged; 252 tests across src/flow_chat/services pass; pnpm run type-check:web clean.
…r-retries fix(agent): unify live model retry budget
`browser.wait` read only `duration_ms`, so the very plausible
`{ "ms": 1800000 }` was dropped and the call fell through to a branch that
returned `{ success: true }` instantly. An agent asked to pause 30 minutes
got "Wait completed" back in milliseconds and moved straight on. The
action documented no parameters at all, so the model had to guess the key,
and even a correct guess was silently capped at 30 seconds.
- Accept the spellings models emit: `duration_ms` / `ms` / `wait_ms` /
`sleep_ms`, plus `seconds` / `secs` variants, numeric strings included.
- Reject a `wait` carrying neither duration nor condition with
INVALID_PARAMS instead of reporting a success that never waited.
- Raise the cap to 60 minutes, in step with AgentWait's MAX_TIMEOUT_MS, and
report `ms` / `requested_ms` / `clamped` so a shortened wait says so.
- Race the sleep against the turn's cancellation token, and stop
`call_impl` from folding Cancelled into an `ok: false` envelope — a stop
during a long pause must not wait out the pause, nor look like a tool
error the model tries to recover from.
- Serve duration waits before session resolution: a pure pause touches no
page, and agents pace themselves long before they open a browser.
- Resolve `{ condition, timeout_ms }`: the condition always wins and any
duration bounds it, rather than sleeping and never looking at the page.
Condition waits keep their previous 15s default, now configurable.
- Document all of it in the tool description, and point repeating schedules
at the Cron tool, which ends the turn instead of pinning it open.
…hots, add bootstrap regression test
Asked to sweep a set of channels every 30 minutes, a Cowork-mode agent replied that it had no cron tool and fell back to chaining long `wait` calls. It was telling the truth: `Cron` was in Claw's tool list and nowhere else, so in Cowork, Team, DeepResearch, and the four shared coding modes the agent could not see it at all. Deferred exposure is not the cause — deferred tools are advertised by name — the tool simply was not in those modes' `default_tools`. ControlHub's `wait` documentation now tells the agent to schedule repeating work with Cron instead of holding the turn open, and ControlHub ships in all of those modes, so the guidance pointed at a tool the agent did not have. - Add `Cron` to Cowork, Team, DeepResearch, and `shared_coding_mode_tools` (agentic / debug / multitask / plan), so it is available everywhere ControlHub is. - Cover the pairing with an invariant test over the built-in modes; it caught the shared coding baseline, which the first pass had missed. - Say in ControlHub's description what to do if Cron is genuinely absent, rather than leaving a chain of long waits as the silent fallback. Scheduling stays behind the normal permission gate — Cron is not read-only and emits a `custom_tool` intent — so this widens what the agent can propose, not what it can do unattended.
Asked to pace itself with a cron job, the agent created one and then kept working for another half hour before stopping on its own. Nothing was broken: `Cron add` returns "Created scheduled job ..." and nothing more, so the call reads as an ordinary success, and no tool can end a turn — only the model stopping ends it. With the loop it just handed to the scheduler still in its head, it drives that loop itself. That is worse than untidy. A scheduled run is submitted as a queued dialog turn at low priority, so it is never run in parallel with the turn already in flight; a round that outlives the interval simply delays the trigger it is racing. The round in question took 32m against a 30m interval, which turns "every 30 minutes" into back-to-back rounds. - Say it in the `add` result, but only when the job actually takes over this turn's cadence: it repeats, and it targets this session. A one-shot reminder, or a job scheduled for some other session, must not cut the current turn short. - Say it in the tool description too, so the model knows before it commits to a plan rather than after — including that a run firing into a busy session is queued, so the interval wants to be longer than a round. - Drop the claim, added with the browser.wait fix, that Cron "ends the turn and re-invokes you". It does not, and a model that believed it would keep the turn open waiting for an end that never comes.
create联动:SessionControl/SessionMessage 新增 worktree 参数(WorktreeSessionOptions), 创建会话时复用 WorktreeService 创建 worktree(detached + registry + 幂等 receipt), 自动命名分支 task/<序号>,失败回滚清理(不遗留孤儿 worktree/会话); ACP agent_type 与 worktree 互斥校验,remote workspace 拒绝。 delete联动:删除会话时按 execution_target.worktree_id 联动 WorktreeService::remove, safety veto(脏/未发布/锁定)保留 worktree 仅告警,remove 失败不阻塞会话删除。 rename联动:重命名会话时同步 WorktreeService::update_display_name(registry 持久化 + WorktreeSummary 透出),分支名沿用 task/<序号> 系不重命名,失败不阻塞。 权限:worktree 创建仅 Commander(或 RBAC 关闭)授权(worktree_creation_authorized)。 涉及文件(9): - agentic/coordination/coordinator.rs(delete/rename 联动) - agentic/tools/implementations/session_control_tool.rs(create 复用 WorktreeService) - agentic/tools/implementations/session_message_tool.rs(create 复用 WorktreeService) - agentic/tools/restrictions.rs(worktree_creation_authorized) - service/worktree/mod.rs(update_display_name + display_name 字段) - services-integrations/git/service.rs(rename_branch) - contracts/core-types/src/worktree.rs(WorktreeSessionOptions/WorktreeSummary.display_name) - contracts/core-types/src/lib.rs(导出 WorktreeSessionOptions) - execution/agent-runtime/src/session_control.rs(worktree 字段 + 校验)
…off + ControlHub Cron + unified retry budget + peer reliability + SDK deps)
…,CI cli-test 编译失败 E0063)
…d dependencies + consolidate contract tests)
- await-holding-lock ×11:测试专用环境锁 9 处加 #[allow] + 注释(与库内既有先例一致),生产代码 1 处(tui_client worktree_repository_status)改作用域先取值后 await,runtime.rs 测试 1 处允许跨断言持有;逻辑等价无死锁 - result_large_err ×1:workspace_search/service.rs cross_validate_empty_result Err 侧改 Box<ContentSearchResult> - manual_unwrap_or_default ×1:session_control_tool.rs list_session_metadata match 改 .unwrap_or_default() - E0432 ×2 + unused import ×1:relay_deploy.rs 删死 import(sync_source_bash/verified_checksum_exports)与未用 import(stage_scripts_command) - suspicious_open_options ×3:config.rs/prompt_stash.rs 锁文件 create(true) 补 truncate(true) - E0716 ×1:desktop/logging.rs 测试 format_args 临时值改为 let 绑定 验证(1.97.1):clippy --all-targets 0 error 0 warning、check --workspace 0 error、CI 路径测试全绿(tool-runtime 1 + cli 3 个 Windows 既有失败已双链归因);rustup default 已切 1.97.1
… unsound) lru 0.12.5 存在 RUSTSEC-2026-0253(LruCache::pop 非 panic-safe, eviction 触发 UAF/双 free),advisory 要求 lru >=0.18.2。 ratatui 0.29.0 锁定 lru ^0.12 无法满足,连带升级: - ratatui 0.29 -> 0.30(ratatui-core 0.1.2 声明 lru ^0.18,lru 升至 0.18.2) - bitflags 精确锁定 =2.11.1 解除为 ^2(ratatui-core 0.1.2 需 ^2.12) - deny.toml 删除失效的 RUSTSEC-2026-0002 ignore(lru 已升级) - startup.rs: Backend 泛型补 B::Error: Send+Sync+'static 约束 (ratatui 0.30 Backend trait 新增关联 Error 类型) 验证(1.97.1):cargo check --workspace 0 error;cli 677 单测通过 (3 个 plugin_source_cli 失败为 Windows 平台 pre-existing,基线同失败); cargo deny advisories/licenses/sources 全过;clippy warning 数零新增。
…r path A single Feishu "send one message" run took 73 tool round-trips. Tracing the log back, most of them came from four defects that compound. **`describe_screen` was blind on macOS.** `macos_foreground_application` built its AppleScript with a `try … end try` *block* in expression position, which AppleScript rejects at compile time (-2741). The command always exited non-zero, so the function always returned `None` — and `describe_screen` derives its target app from that value. Every call reported `foreground_application: null` and `ax_tree_text: null`. The agent reasonably concluded its own tool output was being truncated, stopped trusting results, and fell back to `screencapture` + image-analysis as a substitute for eyes: 10 extra model round-trips that returned prose instead of coordinates and misread the screen repeatedly. Replaced with an in-process `NSWorkspace.frontmostApplication` read, and did the same for `macos_ax_ui::frontmost_pid`, which shelled out to `osascript` on every call — three spawns per `describe_screen`, each ~120ms, each able to block on a System Events AppleEvent timeout. **Enter was permanently refused in text-only mode.** The stale-capture guard is cleared only by a successful capture, but a text-only `screenshot` short-circuits before capturing. So the guard latched: its own error said "call `screenshot` first", calling it changed nothing, and every `click`/Enter stayed blocked. The observed escape was the agent bypassing the tool with raw `osascript … keystroke return`, which skips every check the guard exists to enforce. `describe_screen` (the text-only equivalent of looking) and the text-only `screenshot` stub now waive it. `paste` also clears it unconditionally instead of only when `submit:true` — paste-then-Enter is the common shape and the pointer never moved. **Results were mostly duplicate.** Every `app_state` carried both `tree_text` and `app_state_nodes`, the same nodes re-serialised as verbose JSON. `render_tree_text` already emits every addressable field, nothing consumed the array, and it was 82 KB of a 107 KB result. Dropped, `node_count` kept. **`get_app_state` returned mostly closed menus.** Observing a windowless app produced 188 nodes, 180 of them menu items at zero-size off-screen frames — unclickable until the menu opens. Closed `AXMenu` subtrees are no longer walked; the container stays visible and a note points at `get_app_shortcuts`. Also: `open_app` reported `success: true` for an app running with no window, which is how this run lost ~15 calls rediscovering that `activate` does not reopen an Electron window. It now resolves the bundle id (the launch name, executable name and bundle id are routinely three different strings), polls for a window, retries via `open -b`, and reports `window_count` / `windowless`. Tests: AppleScript templates are now compile-checked with `osacompile`, which catches exactly the class of bug above without executing anything. Drive-by: `embedded_relay_host` tests reserved an ephemeral port, dropped the listener, then assumed it was still free. Harmless on an idle machine and ~80% failing once the suite spawns subprocesses. Port acquisition and the release assertions now retry.
The macOS branch gained bundle_id / process_name / window_count / launch_path; the other two construct the same struct and would not compile without them. Both leave the identity fields None rather than guessing: neither `start` nor `xdg-open` reports what it launched, so there is no pid to resolve identity or a window count from. `window_count: Some(0)` would tell the model the app is definitely windowless when it was simply never measured.
…names Only "Safari" was covered, which never exercises applescript_quote. Compiling the escaped forms of a quote, a backslash and CJK is what proves the escaping matches AppleScript's actual string-literal syntax rather than a plausible guess about it.
The two lines this prompt was missing are the ones that would have ended the observed failure early. When describe_screen returned nulls the agent concluded its own output was truncated and improvised screencapture plus image analysis as a substitute for eyes. State plainly that an empty ax_tree_text is a result with a reason attached (ax_tree_status / ax_tree_note), that re-calling returns the same thing, and that building eyes out of screencapture costs a model round-trip per glance and returns prose instead of coordinates.
…tron apps
Fixing `foreground_application` gave `describe_screen` an app to look at.
It was still walking only 8 levels into the focused window, which is fine
for a native Cocoa app and far too shallow for the Electron / WebView
clients agents are most often asked to drive.
Measured against a real Electron window (focused window only):
depth 8: 17 nodes, 7 actionable, 1 KB
depth 12: 25 nodes, 15 actionable, 2 KB
depth 16: 50 nodes, 40 actionable, 5 KB
depth 20: 207 nodes, 197 actionable, 27 KB
depth 24: 233 nodes, 223 actionable, 31 KB
depth 32: 1289 nodes, 1279 actionable, 206 KB
Seven actionable elements is not enough to find a search field or a send
button, so the tree read as "this app has no AX tree" and pushed the
agent onto OCR and screenshot guessing. The actionable layer appears
around 20; past it the payload grows far faster than the number of things
worth clicking.
Depth is a poor proxy for size, though — a document or a long list can
multiply that 27 KB — so the returned tree is also capped at 60 KB, cut on
a line boundary. The clip announces itself and says a control that is
missing from the view may still exist: an agent that reads a truncated
tree as the whole UI concludes the control is not there and gives up.
The `#[ignore]`d dump test now prints this depth profile, so the constant
can be retuned against evidence rather than intuition.
…e index The 60 KB cap I added in the previous commit sliced the tree at a byte offset. `&str[..n]` panics when `n` falls inside a multi-byte character, so a CJK app tree — the kind most likely to be large enough to hit the cap in the first place — would panic the whole tool call roughly two times in three. Walk back to a char boundary before slicing. The first tests I wrote for this passed against the broken version: repeating a fixed line, and repeating a bare 3-byte character, both happen to land exactly on 60_000. Shifting the content by one and two bytes is what exposes it, so the test now covers all three alignments and was confirmed to fail without the fix.
…use AX tree + AppleScript + OpenAppResult + text-only observation)
ratatui 0.30(ratatui-core 0.1.2)的 Terminal::clear() 为保留光标位置 新增后端光标查询(crossterm Unix 发 DSR ESC[6n 并等待应答),在无人 应答的 PTY 测试环境中超时失败,导致 CI(ubuntu/macos)CLI Tests 的 5 个 TUI 测试进程启动即退出(exit 1),panic 于 terminal_process_contracts.rs:696。 - startup.rs: Terminal::clear() -> backend_mut().clear()(Backend::clear() 仅发 ESC[2J,语义与 0.29 的 Terminal::clear() Fullscreen 分支一致) - mod.rs: with_restored 恢复路径同改,并补 Backend trait import 验证(1.97.1):cargo test -p bitfun-cli 726 passed 0 failed; cargo test -p bitfun-core --lib 2467 passed 0 failed; cargo clippy -p bitfun-cli --all-targets 0 error 无新增 warning; cargo check --workspace 0 error。
…ap osascript Four follow-ups left open by GCWing#2224, each a case where a result told the agent something that was either far too large or quietly untrue. **`get_app_state` was unbounded.** `describe_screen` got a 60 KB cap; the explicit query did not. Measured unbounded output on real Electron apps was 220-390 KB from a single call — roughly 100k tokens spent on one look at one app. Capped at 120 KB (higher, because asking for an app's tree is an explicit request, but still a ceiling), reusing the same clip that lands on a char boundary and announces itself. Applied in `snap_state_json`, so `app_click` / `app_type_text` / `app_scroll` / `app_key_chord` / `app_wait_for` are covered too — they all carry the same post-action tree and shared the same risk. **`analyze_image` reported the resized dimensions as the file's.** Large screenshots get downscaled to fit the provider (repeated 0.75x passes, floor 64px), and `ProcessedImage` kept only the final size — the source dimensions were computed and dropped. So a caller mapping anything the vision model said back to the screen was off by an unknown factor, with nothing in the result hinting at it. `ProcessedImage` now carries `original_width` / `original_height` with `scale()` and `was_resized()`; `analyze_image` and `view_image` report both frames. `analyze_image` also states plainly what its numbers are: any position in the prose is a vision model's estimate in the resized frame, not a measurement and not a click target — use `locate` / `move_to_text` / `describe_screen`, which return real coordinates. Building a coordinate contract on estimated numbers would be worse than saying they are estimates. **`open_app` could block for two minutes.** `activate` sends an AppleEvent and waits for the app to answer; a hung app does not, and macOS's default AppleEvent timeout is 120s, held on a blocking thread with the agent unaware. `Command::output()` has no timeout, so osascript now runs under a polled 10s deadline and is killed past it. A timeout reports as a failed launch the agent can act on, not an opaque io error. **`interaction_state.displays` rode on every result.** On a single-screen machine it repeats what `active_display_id` already says. Sent only when more than one display is attached; `list_displays` and `describe_screen` still report the full list on demand.
唯一一次全量 cargo fmt(主人定标格式化轮)。清理上游 git 历史带入的 688 处未格式化欠账(实际落盘 118 .rs 文件格式化差异)。 纯格式变更,零语义影响:cargo check --workspace 0 error / 三核心 crate 测试全绿 / fmt --check 0 diff / 群聊功能契约回归全绿。 后续回归仓库约定 pnpm run fmt:rs(只格式化改动文件),禁再全量。
任务 A(完整 ChatInput 复用): - GroupChatPane 移除简化输入框,改渲染完整 ChatInput - chatInputRegistration 新增 groupChatMention 扩展点(R-GC-15/16) - ChatInput @@ memberMode 分流渲染 GroupChatMentionPicker(@ 文件提及零回归) - 成员提及经 session-reference context 传递,onSubmit 归一化为 mentionTargets - 新增 buildGroupChatSubmission 纯函数 + 单测(转换/去重) 任务 B(workspace_path 增强): - GroupChatPane 挂载时从 WorkspaceContext 同步真实路径到 store(覆盖直接挂载场景) 任务 C(后端错误结构化 code)登记排后:与 P0×3 工位重叠,等合并后再做 验证:type-check 0 错误;src/flow_chat vitest 188 文件/1575 测试全过
P0-1: MainNav 群聊区块接线(SectionHeader+Plus+GroupChatsSection+CreateDialog+Pane+真实拉人数据源) P0-2: group_chat_send 薄封装复用 tool 层 send_message_impl 派发链,urgent 生效 P0-3: reply_route 非 None + ingest_reply 去 dead_code + 前端 ingestReply + reply_content 落盘 P1-1/2/3: command 补 actor/Claw/反标校验(共享管线自动收敛) P1-4: 双轨根治(create/join/leave/delete/set_mode/send 六合一薄封装) P1-5: GroupChatErrorCode 贯通 tool/command(结构化错误) P1-6: untag 容错 + 锁 Weak+retain P2: 1/2/3/4/8/9/10/11/12/14/15 附带: lib.rs 注册 group_chat_* 12 命令(原未注册,前端必 command not found) 测试: services-core 全量 + bitfun-core 2493 + type-check 0 + vitest 3103 全绿
冲突解决说明(GroupChatPane.tsx 双改合流,授权手动合并): - 冲突1(加载副作用):保留 legacy workspacePath 同步 + p0x3 错误提示(功能互补) - 冲突2(输入区):输入区用 legacy 完整 ChatInput(任务A交付物),保留 p0x3 handleJoin/Leave/SetMode 成员管理逻辑 - 双方逻辑全保留,取更优输入区实现(姬码锋授权方案A)
…use bound AX payloads + image dimensions + cap osascript)
Keep Core product-full explicit, split ACP client/server roles, and guard every product consumer against capability-union regressions. BREAKING CHANGE: bitfun-core default features are now empty; consumers that require the full compatibility surface must enable product-full or select explicit owner features.
…d)! narrow Core/ACP feature assembly; acp/Cargo.toml conflict resolved per 4-principle-3)
Upstream perf(build)! (a4e06ca) emptied bitfun-core default features, leaving rbac_poke_integration / rbac_master_switch compiling against a feature-gated agentic module. Mirror git_contracts pattern with required-features so default "cargo test -p bitfun-core" skips them. Verified: agent-runtime,git,external-sources combination compiles and all 17 RBAC tests pass.
cargo fmt --check surfaced formatting drift in files touched by the group-chat work (scheduler/group_chat_router/group_chat_tool) and the remote workspace policy registry. Format only those files (no global fmt). No behavior change: cargo test -p bitfun-core (146 lib + 17 RBAC) and -p bitfun-desktop --lib (275) stay green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
同步内容
稳定版同步开发版最新基线 \�a982617a\(PR #2177 合并后继续推进):
关系
验证