Repository navigation
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.
Six things the session screen got wrong on device, all reported together. They land in one commit because they overlap in the same few files. The timeline never followed streaming output. `scrollEdge(Edge.Bottom)` only existed inside the floating button's `onClick`, and the list was explicitly `stackFromEnd(false)`, so a bubble growing every 40ms grew below the viewport. Flipping `stackFromEnd` handles the growth case; a `timelineRevision` monitor covers whole-message inserts, and a `stickToBottom` flag reads `isAtEnd()` so scrolling back through history is not fought by the follow. The composer locked to Stop for the whole run. `isStopping` folded dictation and a running turn into one flag, but they sit on opposite sides of the draft: dictation outranks it because the draft is still being spoken, a running turn only outranks it where the draft has nowhere to go. Split into `isVoiceListening` and `isTurnRunning`; remote sessions can hand a message over mid-run, so a draft now wins there and the relay queues it. Stop did not stop. Between sending a message and the server's turn_id arriving, `remoteActiveTurnId()` sliced the local `active-pending-<id>` into `pending-<id>` and sent it as a turn_id. The server read that as `StaleRequestedTurn` and kept running. It now returns '' in that window, which the server resolves as CancelCurrent. The failure was also invisible — it went to `statusText`, which only renders while disconnected — so the stop path now reports through the existing toast channel. AskUserQuestion options were cut off. `OptionRow` was pinned to 36px with no `maxLines` on the label and no `layoutWeight` on the description, so a long option lost its tail with nothing to indicate it. The row now grows with its content and label/description stack instead of competing for one line. Picked images rendered outside the input. They now sit in a strip inside the composer card, left-aligned — `Scroll` centres content narrower than its viewport, which parked a lone thumbnail mid-composer. The plus button opened an attachment panel offering quick prompts and a single image entry. A message can only carry images (`RemoteCommand::SendMessage` has no file channel), so a menu there was a menu of one; plus now opens the album directly, matching local chat. The panel and its orphaned strings are gone. Image encoding also got a size budget, since the transport doubles what it is handed: base64 into the command JSON, then base64 again after AES-GCM.
`is_valid_encrypted_payload` caps the ciphertext at 48 MB, but the route inherited Axum's 2 MB `DefaultBodyLimit`, which rejects the body before the handler — including its auth check — ever runs. A phone attaching a photo got a bare 413 from a route that claimed to accept 24x that. The limit is derived from `MAX_ENCRYPTED_PAYLOAD_BYTES` so the two cannot drift apart again, with slack for the JSON envelope and the base64 nonce. Two tests pin both ends: a 3 MB body reaches the handler, and one past the route ceiling is still refused.
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.
…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.
…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.
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.
Configuring skills for Computer Use failed with "Skill 切换失败". The backend was rejecting the write: `persist_agent_profile_from_value` resolved default tools through `get_mode_defaults()`, which lists only `AgentCategory::Mode` agents, so any sub-agent id fell through to "Agent does not exist". `ComputerUse` is exactly that case — registered as a built-in sub-agent (agent-runtime `agents.rs`, with a contract test pinning it), yet rendered by the agents scene as a core agent card, and the only built-in sub-agent shipping the `Skill` tool. Reads never validated the agent, so the picker listed skills and accepted toggles that could never be saved. Every write path was affected: `replace_mode_skill_selection`, `set_mode_skill_disabled` and `reset_mode_skill_selection` all funnel through the same persist call. The mode-only lookup was correct when it was written, when the function was `persist_mode_config_from_value` and its error read "Mode does not exist". Renaming it to the agent level widened what it promised without widening what it resolved; the agents scene later opened skill editing to sub-agents, which made the gap reachable. - Resolve default tools from modes and sub-agents alike. - Widen profile defaults so canonicalization recognizes sub-agent profiles and normalizes them like any other. - Keep unrecognized profiles instead of pruning them. Canonicalization runs at startup with no workspace, so project-scoped sub-agents are invisible there and would lose their stored selection on every launch now that it can be written at all. Records that no longer deserialize are still dropped, since `get_agent_profile_configs` reads the map as a whole and one bad entry would silently empty it. The runtime side already worked for sub-agents: skill filtering keys on an opaque `mode_id` and shares `resolve_skill_state_for_mode` with the UI listing, so a saved selection takes effect without further changes. The new test pins the invariant rather than the instance: any agent whose default tools include `Skill` must resolve to a profile default.
Scenes stay mounted while the workbench shows another one, so a paused thread goal opened its resume modal from the background: the dialog landed on top of the MiniApp gallery, settings, or whatever scene the user had just clicked into. Gate the prompt on the session scene being the one in front, and treat a change of visit -- leaving the session or opening another one -- as a retract plus re-arm, so the prompt returns when the user opens the paused session again instead of chasing them across the app.
The pending-queue "send now" button refused messages with attachments or structured metadata, refused ACP sessions outright, and dispatch sessions rejected images while a turn was running. All three came from the same root cause: the steering channel was text-only, so anything richer had to wait for a turn boundary. Widen the channel instead of gating on it. `AgentDialogSteerRequest` and `RoundInjection` now carry `attachments` and `metadata` — the same payload shape a turn submission carries — and the execution engine rebuilds them into a multimodal user message at the round boundary, so a steering message with images reaches the model exactly as it would have at a turn boundary. A malformed attachment is rejected up front by the scheduler (while the caller still holds the message) rather than dropped later. The dispatch append path carries attachments end to end as well, under the same structural and device-inline limits `dispatch_continue` already enforces. On the client, "send now" is available in every session state: - a running turn takes the message through steering, payload intact; - a turn that ended between the click and the request no longer surfaces a failure — it falls through to the drain path; - ACP agents own their execution loop and expose no mid-turn injection point, so the button moves the message to the head of the queue and the IDLE drain listener sends it the moment the turn ends. The steering bubble now renders its attachments, so an injected message looks the same as one sent from the composer.
…ache fix(ci): isolate sherpa native cache lifecycle
Scheduled jobs were only reachable from the workspace or session that owned them, so there was no place to see what the whole install is about to run. A Todos entry under the Assistant nav button now opens a scene that aggregates jobs across every workspace in two tiers: runs due within 24 hours as a list, runs further out on a month calendar with a click-through day detail. Todos can be created and edited in place, which also gives manual creation an entry point that does not require opening the owning workspace first. Todos are scheduled jobs rather than a parallel concept, so the scene reads and writes the same cron service the agent's scheduling tool and the per-workspace editors use, and broadcasts on the shared change event so those views stay in step in both directions. A job whose next run is gone is grouped as paused, done, or invalid instead of disappearing, so it stays reachable. Occurrence times are derived the way the backend derives them, mirroring the Rust `cron` crate rather than classic crontab: 5/6/7 fields, day-of-week 1-7 with Sunday as 1, and day-of-month ANDed with day-of-week. Getting those wrong would advertise runs that never happen, so they are pinned by tests. Schedules carrying an explicit timezone are not expanded client side — a browser's only timezone database is `Intl`, which the i18n contract reserves for the shared formatters — so they fall back to the next run the scheduler itself reports rather than an approximation. Each tier caps occurrences per job, per day on the calendar, so a five-minute job marks every day it runs instead of flooding one. Also extracts the pure draft and schedule conversion helpers out of ScheduledJobsView into scheduledJobDraft.ts so both editors build their requests from the same code. That move is mechanical, with no behavior change to the existing view.
- map common external Agent tools to provider-neutral BitFun capabilities - add owner-scoped bulk controls with exact reviewed decision sets - surface safe MCP startup failures and actionable source states
…ror-handling fix(harmonyos): wrap SDK BusinessError so ArkTS compile warnings clear
Two provider ids serving the same models is a real deployment, not a mistake: a second route to the same vendor, mounted under its own id so it can carry its own key. The picker listed the catalog per provider, so a user with such a route saw every model twice — same name, same vendor, nothing to choose between, and only the value prefix telling the rows apart. Publish one row per model id. The provider carrying the session's current model owns the shared ids, so the list stays on the route that actually answers, and the checked row keeps its provider even when that provider's listing failed. A provider left with no models of its own drops out of the picker; one that has models nobody else serves keeps its group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An ACP agent publishes its modes as a `mode`-category config option, and they were rendered inside the model dropdown, under the model list and above the fast-mode switch. Two unrelated choices shared one button: the trigger named the model, so nothing on the composer said which mode was in force, and opening the model list to change the permission mode read as the wrong menu. Split the mode into its own trigger beside the model, built on the same dropdown, keyboard, and positioning code as the reasoning preset picker, so the composer now reads model, mode, reasoning. The trigger shows the mode name. Protocol behaviour is untouched: the same `session/set_config_option` call, the same lock semantics once a conversation has started, the same option ids and test hooks. An agent that publishes a mode and no models keeps a full picker — the mode trigger takes over the context-usage badge and the tooltip that the model trigger would have carried, so nothing is lost when the model button is not rendered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(dsh-acp): offer a per-session model picker
Creating a session in a workspace other than the open one first sent set_workspace, so the phone moved the desktop's own view before the create landed. CreateSession already binds whatever workspace_path it is handed, so that round trip bought nothing and cost the desktop its place — harmless enough with one machine in view, wrong once the sidebar lists several. Carrying the path on CreateSessionOptions collapses the three-branch dance around it, along with the recovery path that existed only to undo a host workspace this no longer touches. Co-authored-by: Cursor <cursoragent@cursor.com>
The remote surface only ever faced one machine: the workspace section drew the desktop the phone was currently driving, and reaching another one meant moving the global control target, which took the first one out of view. A console that can only see where it happens to be pointed is not a console. deviceRpc is already stateless — every call carries the relay URL, session and target device — so a read-only directory can fan out without touching RemoteSessionManager's single transport, which still owns the conversation. Expanding a device is what dials it; offline rows are never dialed at all, which is what keeps opening the drawer off a row of 130s timeouts. Each device gets its own SidebarDeviceGroup because SessionListProjectionCache holds a single slot keyed on the workspace, so one instance serving N devices would miss on every rebuild. Catalogs persist per device_key and the live device's own catalog is captured on publish, so a device keeps its rows after the control target moves on. Co-authored-by: Cursor <cursoragent@cursor.com>
Scoped to the HarmonyOS app: what was verified against main, why the observation surface fans out per device while the conversation surface stays on one, and why cross-device execution status is out of reach until the remote SessionInfo payload carries a status field at all.
…ooked at Device presence was fetched once at cold start and never again. The relay keeps presence in a live socket registry, so its answer is current whenever it is asked — the phone simply stopped asking, and a desktop that came online after launch stayed grey in the sidebar until the app was killed. Returning to the foreground and opening the sidebar each refetch, with a 15s freshness window so the two firing together on a cold start cost one request rather than three.
Tapping a session on another desktop tears the current link down before dialling the new one, so a desktop that has gone away used to cost the user both machines for two minutes: AccountDeviceCommandTransport dropped the caller's timeout on the floor and always took deviceRpc's hardcoded 130s, nothing on screen said a switch was underway, and when it finally failed the phone was left on no device at all with polling and the heartbeat stopped. The timeout now travels from the transport through to deviceRpc, and the connect handshake passes 15s of its own — a desktop that is running answers get_workspace_info from memory, so silence there means it is gone, not busy. The switch seam shows a toast, and a failed dial restores the device it started from, snapshotting it before the teardown makes it unreadable. Also from the same round on the device: the active desktop rendered grey because the ForEach key for the unpaired placeholder collided with the real row for that same machine, freezing the child on an object nothing updates; and a live link now outranks the relay's presence answer, which is hearsay from a socket registry that lags. Verified against two desktops, the failure path reproduced by freezing one so the relay kept reporting it online.
The phone decides; it never executes. Detached Dispatch already has the shape this needs — the phone remote-controls A, A is the controller that owns the Git baseline, and B is the target that runs the worker. What was missing was never authority, only an entry point on the phone. So the design is written as a checklist of decisions, not of features: for every decision the desktop can make about a job, the phone needs a verb, and every desktop command left out has to say why it is work rather than a decision. What cannot travel is the conversation — the submit request carries no history, on any client — and that boundary is stated rather than papered over.
…vice-directory feat(harmonyos): show every account desktop in the sidebar
The in-app language never changed because RemoteI18n only had a hardcoded zh-CN map and no locale state. Keep catalogs, persistence, and Settings selection on the phone so the current surface rebuilds immediately after a switch. Co-authored-by: Cursor <cursoragent@cursor.com>
Vendors flashgrep v0.2.15, aligns BitFun with the daemon's new base-snapshot
protocol, and closes the gaps that made the managed index either wrong or
inscrutable from the UI.
Protocol (breaking upstream change): `RepoStatus.rebuild_recommended` is gone,
replaced by base-delta fields; `SearchParams.allow_scan_fallback` and the
BitFun-only `QuerySpec.before_context`/`after_context` are gone too. The old
required field would have failed every status-bearing response, so the binary
bump and the protocol change land together. The "rebuild recommended" badge
becomes "index catching up", driven by `base_advance_target_head`.
Line text: the daemon's four search modes return positions only, so BitFun was
rendering line numbers as content ("path:73:line 73"). Content search now goes
through `search/grouped_line_matches` and hydrates text from disk, locally via
`workspace_search/line_hydration.rs` and over SSH via a single batched awk pass
(`remote_line_hydration.rs`) rather than one round trip per file. Both paths
share `services-core::filesystem::content_preview`.
Auto-index policy: index only workspaces with roughly 2000+ indexable files.
Below that the measured win is 8 ms to 0.2 ms while the index inflates worst
(3.3x). The file count is gathered in two commands because
`--others --exclude-standard` costs 4.2 s on chromium against 89 ms for
`--cached`, so the tracked pass returns early once the threshold is met.
Builds run through a queue with a cross-workspace disk budget.
Policy visibility: the daemon reports `needs_index` both while the policy is
still evaluating and after it declined, so the UI could only hedge. The
decision now rides along with the status (`WorkspaceIndexStatus.auto_index`),
and a small workspace reads "no index needed" with the count and threshold
instead of an ambiguous sentence that never changes. Remote workspaces have no
BitFun-side policy and report `None`.
A workspace that is not a Git worktree can never be indexed, which is a
property of the folder rather than a fault, so it stays on the neutral
indicator instead of turning red.
Grep's `-A`/`-B`/`-C` route to ripgrep, because the daemon has no context-line
support and the flags were previously dropped on the wire without a word.
`ContentSearchRequest` loses its context-line fields entirely rather than
keeping them unread: a field that looks like it carries context but is silently
discarded is exactly how the original defect happened, so the indexed path can
no longer express the request at all.
…ex-strategy feat(search): make the workspace index policy visible and correct
feat(harmonyos): switch the phone UI between Chinese and English
Project the resolved per-round permission mode into tool custom data so Task delegation forwards the parent's effective mode instead of falling back to the global default. Extend tool-context materialization coverage for auto_approve mode.
…fixes fix(sessions): stabilize durable history reconciliation
Align the code-repo docs with architecture/guideline/specs/plans, externalize user guides to bitfun_doc, and keep GCWing norms via guideline ops pages plus path-only product/audit reference updates. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep product-facing setup docs in the code repo and retarget README / RemoteConnectDialog links away from bitfun_doc. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove gitcode/bitfun_doc links and out-of-repo placement rules so governance docs focus only on in-repo documentation scope. Co-authored-by: Cursor <cursoragent@cursor.com>
Owner
|
The document status is inconsistent with the current repository, and since the document affects AI behavior—yet repository-side maintenance of it will be reduced going forward—this PR will not be merged. |
Owner
|
Thank you for your contribution. The If your changes are still needed, please reapply them on a fresh branch based on the new We apologize for the disruption and appreciate your understanding. |
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.
Align the code repository around four authoritative documentation buckets —
architecture/,guideline/,specs/, andplans/— and turn the rootAGENTSfiles into progressive-disclosure indexes.Summary
AGENTSguidance through standards, architecture, cross-cutting, and task-based maps.tokio/fullprohibition and owner-scoped Rust verification.Type and Areas
Type: documentation / repository governance
Areas: root
AGENTSand contributor entrypoints,docs/, Web UI documentation URLs, appearance audit documentation pathsMotivation / Impact
The old layout mixed stable architecture, operational rules, feature designs, plans, product setup guides, research, and temporary-process paths. The new layout gives each durable document one canonical owner while keeping high-frequency rules discoverable within two hops from
AGENTS.md.There is no product-logic change. User-visible impact is limited to documentation/help links, and legacy public paths remain reachable for already released clients.
Migration Map
docs/development/{i18n,releasing,ui-testids*}docs/guideline/docs/remote-connect/feishu-bot-setup*docs/guideline/feishu-bot-setup*; old paths retained as compatibility pointersdocs/verify-downloads*docs/guideline/verify-downloads*; old paths retained as compatibility pointersdocs/features/{agent-hooks*,remote-workspaces,session-runtime-usage-report-design}docs/specs/; old public paths retained as compatibility pointersdocs/superpowers/specs/*docs/specs/*docs/superpowers/plans/*docs/plans/*-plan.mddocs/sdlc-harness/architecture/*anddesign.mddocs/architecture/*anddocs/architecture/sdlc-governance-architecture.mddocs/sdlc-harness/features/*, product requirements, metrics, and traceabilitydocs/specs/*docs/sdlc-harness/{implementation-plan,agent-workflow-staged-plan}.mddocs/plans/sdlc-governance-*.mddocs/sdlc-harness/governance/self-governance-notes.mddocs/guideline/self-governance-notes.mddocs/sdlc-harness/research/external-research.mddocs/specs/sdlc-governance-external-research.mdas a dated non-normative referencedocs/performance/01-compile-performance.mddocs/plans/2026-08-14-compile-and-dependency-governance-plan.mddocs/performance/02-04reviewsreferencedocuments underdocs/specs/; implementation requires revalidationdocs/sdlc-harness/README.mddocs/architecture/sdlc-governance-architecture.mddocs/specs/templates/plan.mdintroduced by the initial migrationdocs/plans/templates/plan.mdCompatibility / Reviewer Notes
bitfun_docrepository.docs/README.md.Verification
git diff --checkpnpm run check:repo-hygienepnpm --dir src/web-ui run gen:types— 34 tests passedpnpm run type-check:webpnpm run appearance:contract-audit— passed; 16 existing advisory warningsdocs/README.mdanddocs/guideline/docs-governance.mdChecklist