Skip to content

Commit 4a43663

Browse files
committed
Merge main into PR 6984 and preserve objective-aware acceptance
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
2 parents 501c880 + 21322fb commit 4a43663

235 files changed

Lines changed: 8016 additions & 881 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
A local-first control plane and research workbench for agents and teams.<br>
88
<sub>Keep goals, decisions, ownership and evidence across sessions. Work with the runtimes you already use: Codex, Claude Code, DeepSeek Harness and others.</sub>
99

10-
[![Release](https://img.shields.io/github/v/release/loopx-project/loopx?color=181717)](https://github.com/loopx-project/loopx/releases/latest) [![License](https://img.shields.io/badge/license-Apache--2.0-181717)](LICENSE) [![TypeScript core](https://img.shields.io/badge/core-TypeScript-3178C6?logo=typescript&logoColor=white)](docs/architecture/rfcs/typescript-control-plane-migration-v0.md) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/XmGgQyCFZd)
10+
[![Release](https://img.shields.io/github/v/release/loopx-project/loopx?color=brightgreen)](https://github.com/loopx-project/loopx/releases/latest) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) [![TypeScript core](https://img.shields.io/badge/core-TypeScript-3178C6?logo=typescript&logoColor=white)](docs/architecture/rfcs/typescript-control-plane-migration-v0.md) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
11+
12+
[![Local first](https://img.shields.io/badge/control--plane-local--first-brightgreen.svg)](docs/public-private-boundary.md) [![Loop Agents](https://img.shields.io/badge/status-loop%20agents%20supported-brightgreen.svg)](docs/product/release-readiness.md) [![GitHub contributors](https://img.shields.io/github/contributors-anon/loopx-project/loopx?color=blue)](https://github.com/loopx-project/loopx/graphs/contributors) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/XmGgQyCFZd)
1113

1214
[Get started](#quick-start) · [Why LoopX](#why-loopx) · [See it in action](#see-loopx-in-action) · [Evidence](#evidence) · [Contribute](#contributing) · [Docs](https://loopx-project.github.io/loopx/docs/) · [简体中文](README.zh-CN.md)
1315

‎README.zh-CN.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
面向 Agent 与团队的本地优先控制面和研究工作台。<br>
88
<sub>跨会话保留目标、决策、分工与证据,连接你已经在用的 Codex、Claude Code、DeepSeek Harness 等运行时。</sub>
99

10-
[![Release](https://img.shields.io/github/v/release/loopx-project/loopx?color=181717)](https://github.com/loopx-project/loopx/releases/latest) [![License](https://img.shields.io/badge/license-Apache--2.0-181717)](LICENSE) [![TypeScript core](https://img.shields.io/badge/core-TypeScript-3178C6?logo=typescript&logoColor=white)](docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/XmGgQyCFZd)
10+
[![Release](https://img.shields.io/github/v/release/loopx-project/loopx?color=brightgreen)](https://github.com/loopx-project/loopx/releases/latest) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) [![TypeScript core](https://img.shields.io/badge/core-TypeScript-3178C6?logo=typescript&logoColor=white)](docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md) [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
11+
12+
[![Local first](https://img.shields.io/badge/control--plane-local--first-brightgreen.svg)](docs/public-private-boundary.md) [![Loop Agents](https://img.shields.io/badge/status-loop%20agents%20supported-brightgreen.svg)](docs/product/release-readiness.md) [![GitHub contributors](https://img.shields.io/github/contributors-anon/loopx-project/loopx?color=blue)](https://github.com/loopx-project/loopx/graphs/contributors) [![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/XmGgQyCFZd)
1113

1214
[快速上手](#快速开始) · [为什么需要 LoopX](#为什么需要-loopx) · [看看实际使用](#看看-loopx-如何工作) · [证据与案例](#证据) · [参与贡献](#贡献) · [文档](https://loopx-project.github.io/loopx/docs/) · [English](README.md)
1315

‎apps/desktop/loopx-control-plane/README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ continue beyond thirty seconds; stalled headers or body reads remain bounded.
6060
The archive client limits apply only after feed discovery; the feed keeps its
6161
existing check deadline (45 seconds manually, 30 seconds in the background).
6262
Signature verification still precedes cache publication or installation.
63+
The existing recovery and workspace update panels distinguish a download timeout,
64+
an interrupted network request, an unavailable package and a rejected signature.
65+
They keep the coarse cause and retry action without exposing transport URLs or
66+
signature diagnostics. Unclassified failures retain the general download/verification
67+
message; a failure never certifies that an update was installed.
6368
Progress and failures use the
6469
existing update panel; background IO does not hold the service supervisor or
6570
restart a running workspace.
@@ -70,6 +75,10 @@ restart a running workspace.
7075
才写入准备缓存或安装。现有更新面板显示进度与失败,后台下载不阻塞服务监督或重启
7176
正在使用的工作区。
7277

78+
启动恢复页与工作区更新面板分别提示下载超时、网络请求失败、安装包不可用和签名
79+
校验失败,只展示可操作的原因与重试入口,不暴露传输地址或签名内部错误。
80+
无法分类的失败仍保留通用提示,失败不会被显示为已安装。
81+
7382
Startup automatically uses the newest qualified local runtime it can establish.
7483
It compares package versions; equal versions with source revisions use ancestry
7584
from the fixed official GitHub repository. Installation time and lexical SHA
78.5 KB
Loading
99.7 KB
Loading
99 KB
Loading
45.9 KB
Loading

‎apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,21 @@ fn check_error(error: tauri_plugin_updater::Error) -> &'static str {
402402
_ => "update_check_failed",
403403
}
404404
}
405+
406+
fn download_error(error: tauri_plugin_updater::Error) -> &'static str {
407+
use tauri_plugin_updater::Error;
408+
match error {
409+
Error::Reqwest(error) if error.is_timeout() => "update_download_timeout",
410+
Error::Reqwest(_) => "update_network_failed",
411+
// The pinned updater uses Network for a non-success archive response.
412+
// Do not expose its URL/status message or mislabel it as a bad signature.
413+
Error::Network(_) => "update_download_failed",
414+
Error::Minisign(_) | Error::Base64(_) | Error::SignatureUtf8(_) => {
415+
"update_signature_invalid"
416+
}
417+
_ => "update_download_or_signature_failed",
418+
}
419+
}
405420
// Environment telemetry for the recovery diagnostics: coarse, non-PII facts
406421
// that separate "fresh Mac without a usable Python" from OS-specific defects.
407422
// No paths, environment variables or process output beyond the probed version.
@@ -704,7 +719,7 @@ async fn download_update(
704719
update
705720
.download(on_chunk, || {})
706721
.await
707-
.map_err(|_| "update_download_or_signature_failed".into())
722+
.map_err(|error| download_error(error).into())
708723
}
709724

710725
async fn find_update<R: tauri::Runtime>(

‎apps/desktop/loopx-control-plane/src-tauri/src/maintenance_transfer_tests.rs‎

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ impl Server {
6363
let text = String::from_utf8_lossy(&request[..size]);
6464
let path = text.split_whitespace().nth(1).unwrap_or("");
6565
let mode = path.trim_start_matches('/').trim_start_matches("feed/");
66+
if !path.starts_with("/feed/") && mode == "not-found" {
67+
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
68+
return;
69+
}
6670
if (!path.starts_with("/feed/") && mode == "headers") || mode == "feed-stall" {
6771
pause(&done, Duration::from_secs(40));
6872
return;
@@ -147,7 +151,12 @@ fn native_update_transport_checks_signatures_truncation_and_cache_readback() {
147151
let app = app(KEY);
148152
let server = Server::new();
149153
tauri::async_runtime::block_on(async {
150-
for mode in ["fast", "tampered", "truncated"] {
154+
for (mode, expected_error) in [
155+
("fast", None),
156+
("tampered", Some("update_signature_invalid")),
157+
("truncated", Some("update_network_failed")),
158+
("not-found", Some("update_download_failed")),
159+
] {
151160
let update = find_update(
152161
app.handle(),
153162
"stable",
@@ -158,8 +167,8 @@ fn native_update_transport_checks_signatures_truncation_and_cache_readback() {
158167
.unwrap()
159168
.unwrap();
160169
let result = download_update(&update, |_, _| {}).await;
161-
if mode != "fast" {
162-
assert!(result.is_err(), "invalid archive was accepted: {mode}");
170+
if let Some(expected) = expected_error {
171+
assert_eq!(result.unwrap_err(), expected, "archive mode: {mode}");
163172
continue;
164173
}
165174
let bytes = result.unwrap();
@@ -181,6 +190,44 @@ fn native_update_transport_checks_signatures_truncation_and_cache_readback() {
181190
});
182191
}
183192

193+
#[test]
194+
fn background_download_failures_keep_their_cause_without_blocking_local_services() {
195+
let app = app(KEY);
196+
let server = Server::new();
197+
tauri::async_runtime::block_on(async {
198+
for (mode, expected) in [
199+
("tampered", "update_signature_invalid"),
200+
("truncated", "update_network_failed"),
201+
("not-found", "update_download_failed"),
202+
] {
203+
let state = Maintenance::default();
204+
state.publish("ready", json!({}));
205+
let generation = state.admit_background_update().unwrap();
206+
let result = state
207+
.run_background_update(
208+
generation,
209+
"stable",
210+
find_update(
211+
app.handle(),
212+
"stable",
213+
server.feed(mode),
214+
Duration::from_secs(2),
215+
),
216+
|update| async move {
217+
download_update(&update, |_, _| {}).await?;
218+
panic!("a rejected archive cannot be published to the cache");
219+
},
220+
)
221+
.await;
222+
assert_eq!(result.unwrap_err(), expected);
223+
assert_eq!(state.status_snapshot()["details"]["code"], expected);
224+
assert_eq!(state.status_snapshot()["details"]["background"], true);
225+
assert_eq!(state.recover_connected(|| 1), Some(1));
226+
assert!(state.pending.lock().unwrap().is_none());
227+
}
228+
});
229+
}
230+
184231
#[test]
185232
fn native_feed_keeps_its_short_deadline() {
186233
let app = app(KEY);
@@ -292,7 +339,10 @@ fn stalled_update_transfer_remains_bounded() {
292339
.unwrap()
293340
.unwrap();
294341
let start = Instant::now();
295-
assert!(tauri::async_runtime::block_on(download_update(&update, |_, _| {})).is_err());
342+
assert_eq!(
343+
tauri::async_runtime::block_on(download_update(&update, |_, _| {})).unwrap_err(),
344+
"update_download_timeout"
345+
);
296346
assert!(
297347
start.elapsed() < Duration::from_secs(35),
298348
"stalled {mode} transfer was not bounded"

‎apps/desktop/loopx-control-plane/static/boot.js‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ const errors = {
112112
update_feed_invalid: "更新源格式异常。请稍后重新检查。",
113113
update_platform_unavailable: "此通道尚无适用于本机的更新包。",
114114
update_check_timeout: "检查更新超时。请稍后重试。",
115-
update_network_failed: "无法连接更新服务器。请检查网络后重试。",
115+
update_network_failed: "更新网络请求失败,尚未安装。请检查连接后重试。",
116+
update_download_timeout: "更新包下载超时,尚未安装。请检查网络后重试。",
117+
update_download_failed: "更新服务器未能提供安装包,尚未安装。请重新检查更新。",
118+
update_signature_invalid: "更新包未通过签名校验,尚未安装。请重新检查更新。",
116119
update_download_or_signature_failed: "更新包下载或签名校验失败,尚未安装。请重新检查更新。",
117120
app_install_failed: "App 安装未能完成,本次更新未生效;已确认当前版本完好且运行时可用,可直接重启继续使用,或重新检查更新后再试。",
118121
app_install_incomplete: "App 安装中断,且无法确认当前版本是否完整,请勿直接重启。请在恢复与更新面板还原上一版本(或重新安装)后再试。",

0 commit comments

Comments
 (0)