Skip to content

sync: taiji 稳定版同步最新基线 aa982617a(群聊交付 + CI 治理 + 上游合流) - #2232

Merged
GCWing merged 48 commits into
GCWing:custom/taiji-unofficialfrom
1688mengdie:custom/taiji-unofficial
Aug 12, 2026
Merged

sync: taiji 稳定版同步最新基线 aa982617a(群聊交付 + CI 治理 + 上游合流)#2232
GCWing merged 48 commits into
GCWing:custom/taiji-unofficialfrom
1688mengdie:custom/taiji-unofficial

Conversation

@1688mengdie

Copy link
Copy Markdown

同步内容

稳定版同步开发版最新基线 \�a982617a\(PR #2177 合并后继续推进):

  • 群聊功能完整交付(Wave 1-7 + P0x3 大修复轮 + 回执闭环 + MainNav 接线)
  • CI 治理门禁修复(i18n key + appearance 注册 + CJK 清理 + remote workspace policy)
  • 上游合流 20260812-01/02(computer-use + perf build feature assembly)
  • 格式化轮 + worktree bug 修复

关系

验证

  • cargo check -p bitfun-core 0e0w
  • 群聊契约测试 20 passed
  • 前端 type-check 0 error + vitest 16 tests 全绿

bobleer and others added 30 commits August 10, 2026 22:16
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.
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)
…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。
bobleer and others added 18 commits August 12, 2026 00:05
…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.
@GCWing
GCWing merged commit 7f72b84 into GCWing:custom/taiji-unofficial Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants