Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
68fa6cf
test(ci): model both PR-number fields in concurrency fixtures
songoow Oct 14, 2026
ee08331
ci: queue superseded main pushes instead of cancelling the running qu…
songoow Oct 14, 2026
1305980
docs(ci): record six Python test shards
songoow Oct 14, 2026
a33a523
Merge current main into the main-push queue change
songoow Oct 14, 2026
5c13b80
Revert "ci: queue superseded main pushes instead of cancelling the ru…
songoow Oct 15, 2026
34becce
Merge current main to refresh CI fixture qualification
songoow Oct 15, 2026
de01ac0
Merge commit '8f19e614ac2143589458a7f3f31e9c75c413efe3' into codex/pr…
songoow Oct 15, 2026
150bb93
Merge commit 'a59aed703374040240f23b2e51ddaf209fcbacc4' into codex/pr…
songoow Oct 15, 2026
14cadd4
Merge commit '76cdb27da47b8d83c137f287318e1aa7bb847539' into codex/pr…
songoow Oct 15, 2026
60633ee
test(desktop): qualify rollback with released signed app bundles (#6515)
loopx-agent Oct 15, 2026
9d5a936
feat(chat): interact with bound Codex App conversations (#6482)
loopx-agent Oct 15, 2026
0dfacfb
fix(benchmark): route native and LoopX workers through the same custo…
songoow Oct 15, 2026
bed74b7
docs(benchmark): document matched model routing and credential bounda…
songoow Oct 15, 2026
24d135d
fix(opencode2): fall back to the experimental session wait route
chyax98 Oct 15, 2026
f0dd993
build(deps): bump js-yaml in /packages/dsh-loopx-plugin
dependabot[bot] Oct 15, 2026
df5308c
fix(deps): keep the plugin lockfile override and bump only js-yaml
huangruiteng Oct 15, 2026
5c59187
fix(desktop): expose direct recovery after interrupted App replacement
loopx-agent Oct 15, 2026
71fda08
docs(desktop): illustrate interrupted update recovery
loopx-agent Oct 15, 2026
f1fb68e
fix(chat): explain turns interrupted by an App restart
loopx-agent Oct 15, 2026
cb78d5e
fix(chat): allow ordinary project Git updates to local skills (#6519)
loopx-agent Oct 15, 2026
b002bd8
Merge pull request #6520 from loopx-project/codex/edgebench-platform-…
huangruiteng Oct 15, 2026
e0cfa33
Merge pull request #6522 from chyax98/fix/opencode2-wait-route-fallback
huangruiteng Oct 15, 2026
9f56bdc
fix(ego-reader): report same-origin redirect destinations
loopx-agent Oct 15, 2026
5e58ed4
Merge pull request #6529 from loopx-project/codex/ego-source-redirect…
huangruiteng Oct 15, 2026
04b6e1b
docs(getting-started): position the Personal Workspace as the support…
songoow Oct 15, 2026
ca54006
fix(benchmark): restore selected checkpoint research files (#6518)
loopx-agent Oct 15, 2026
0e0a3e0
fix(benchmark): reuse native checkpoint reader through sealing (#6525)
loopx-agent Oct 15, 2026
cd716f0
fix(lark): overlap group source identity and message reads (#6523)
loopx-agent Oct 15, 2026
871c100
fix: retain autonomous replan decisions in stride followthrough (#6532)
loopx-agent Oct 15, 2026
a27f4c0
fix(architecture): restore module and semantic owner guards (#6530)
songoow Oct 15, 2026
8826408
fix(desktop): allow cold wheel identity qualification (#6526)
loopx-agent Oct 15, 2026
c243fe6
Merge remote-tracking branch 'upstream/main' into codex/pr6416-ci-ref…
songoow Oct 15, 2026
af56c5f
fix(quota): recover receipt-bound todos omitted from compact views (#…
huangruiteng Oct 15, 2026
4b838ba
Merge pull request #6416 from songoow/codex/ci-main-push-queue-20261014
songoow Oct 15, 2026
4ae2dcd
feat(acceptance): prepare owner-pinned delivery checks in the workben…
loopx-agent Oct 15, 2026
5017b02
fix(runtime): recover Windows lock creation and exact fixture cleanup…
loopx-agent Oct 15, 2026
2abf983
feat(workspace): show a first-Goal home instead of a fresh-install he…
songoow Oct 15, 2026
d3e0d01
fix(claude-goal-mode): refuse to rewrite an unreadable settings.json …
JasonBuildAI Oct 15, 2026
f804794
fix(contract): fail closed when the boundary scan cannot list a direc…
JasonBuildAI Oct 15, 2026
04cbc18
fix(benchmark): align controller checkpoint completion deadlines (#6536)
loopx-agent Oct 15, 2026
d8838ee
fix(turn): recover the original Turn without fresh selection (#6516)
hhyykk Oct 15, 2026
34f615d
feat(workspace): follow attached Agent work in a stable page (#6540)
loopx-agent Oct 15, 2026
fd19645
fix(runtime): fence archive against goal recreation (#6398)
Duang777 Oct 15, 2026
98d9291
fix(quota): restore canonical sources for admitted lifecycle replans
huangruiteng Oct 15, 2026
c08e561
fix(replan): isolate original lifecycle recovery from later user gates
loopx-agent Oct 15, 2026
9c6f4d4
fix(todos): default new work to P1 and preserve successor priority in…
huangruiteng Oct 15, 2026
7c1f12a
fix(benchmark): stop passing the retired planning cap from LHTB run.s…
songoow Oct 15, 2026
27f74c0
Merge main priority and Monitor updates into the retirement branch
loopx-agent Oct 15, 2026
e97c5e7
Test DSH closeout against canonical File authority
loopx-agent Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ jobs:
group: loopx-python-test-shard-${{ matrix.shard }}
queue: max
cancel-in-progress: false
# All four shards were still progressing at 83-94% when the 45m job
# All shards were still progressing at 83-94% when the 45m job
# deadline cancelled run 37916979857. Reserve setup/reporting headroom
# without changing test-level deadlines, collection or worker count.
timeout-minutes: 60
Expand Down
44 changes: 44 additions & 0 deletions apps/desktop/loopx-control-plane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ previous runtime. Core's installation-only doctor qualifies non-editable wheel
fingerprints: a local reuse fence, not publisher or source attestation. Older
CLIs without that readback can use the automatic App-owned fallback.

Wheel qualification has a separate ten-second subprocess deadline because it
starts Core, checks the TypeScript runtime and hashes package contents. The
one-second environment/version probes remain unchanged. A slow valid wheel can
therefore qualify without asking the user to repair it; invalid or timed-out
qualification still fails closed, and timed-out process groups are terminated
and reaped. This does not speed up doctor or certify every cold-start workload.

A launch-time `LOOPX_BIN` remains the developer's explicit pin. Development mode
also accepts a source runtime without promoting an installation. A previously
saved path is a discovery candidate, not a permanent version pin. Corrupt
Expand Down Expand Up @@ -305,6 +312,43 @@ cd ..
npm run build
```

On macOS, also qualify a released artifact using the opt-in checks below
(run from `src-tauri`, replacing the paths with downloaded release artifacts).
Keep the updater archive's `.sig` file beside it:

```bash
LOOPX_TEST_APP="/path/to/LoopX.app" cargo test --lib \
real_release_backup_restores_damage_and_rejects_tampering -- --ignored
LOOPX_TEST_SIGNED_ARCHIVE="/path/to/LoopX.app.tar.gz" cargo test \
--test signed_bundle -- --ignored
```

The first check copies the signed App into temporary directories, then exercises
the production restore path after removing its executable, a sealed runtime
resource, or the entire target bundle. It also rejects a tampered backup before
journaling or replacement and checks that the source artifact remains intact.
The second verifies the updater archive with the shipped public key and rejects
modified bytes. Neither check launches the App or writes to an installed App or
user data. These qualify the native recovery and signature boundaries; they do
not replace packaged UI checks of download failure, restart, or runtime handoff.

### Recover An Interrupted App Update

If replacement leaves the App incomplete and a backup is available, the update
panel exposes **Restore previous version** directly. Restore it, then choose
**Restart to finish**. This removes two navigation disclosures from the failure
path; choosing a healthy installation's rollback remains under Advanced options.
The native updater still verifies the backup before replacement. An unavailable
or invalid backup reports its cause without recommending an unavailable runtime
repair. Restoring the App does not overwrite a separately managed CLI.

| Previous recovery panel | Direct recovery action |
| --- | --- |
| ![Recovery hidden in Advanced options](docs/recovery-before.png) | ![Restore action beside the incomplete-installation message](docs/recovery-after.png) |

These packaged-panel captures use synthetic native status for presentation
comparison; they do not certify an installed update or an executing task's recovery.

`npm run build` produces the configured platform bundles under
`src-tauri/target/release/bundle/`. The release workflow builds macOS `.dmg`
and `.app.zip` artifacts on macOS, Windows `.msi` and `.exe` artifacts on
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,11 @@ pub(crate) fn python3_environment() -> (bool, Option<String>) {
}

const VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(1);
// Installation qualification boots Core, checks the TypeScript runtime and
// hashes the installed package. It is not a version-banner query: a valid cold
// wheel can exceed one second. Keep that work bounded without weakening Core's
// qualification or extending the lightweight environment probes.
const INSTALLATION_PROBE_TIMEOUT: Duration = Duration::from_secs(10);

pub(crate) fn timed_output(command: Command) -> Option<std::process::Output> {
timed_output_with_timeout(command, VERSION_PROBE_TIMEOUT)
Expand Down Expand Up @@ -986,7 +991,7 @@ fn runtime_identity_for_executable_with_path(
let mut command = Command::new(canonical);
configure_runtime_environment(&mut command);
command.args(["--format", "json", "doctor", "--installation-only"]);
let output = timed_output(command)?;
let output = timed_output_with_timeout(command, INSTALLATION_PROBE_TIMEOUT)?;
if !output.status.success() {
return None;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
use super::*;

#[cfg(unix)]
fn installation_probe_fixture(body: &str) -> (tempfile::TempDir, PathBuf) {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("fixture directory");
let bin = root.path().join("bin");
fs::create_dir(&bin).expect("fixture bin");
let executable = bin.join("loopx");
fs::write(
&executable,
format!("#!/bin/sh\n[ \"$*\" = \"--format json doctor --installation-only\" ] || exit 2\n{body}\n"),
)
.expect("fixture executable");
fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
(root, executable)
}

#[test]
#[cfg(unix)]
fn installation_probe_allows_cold_qualification_beyond_version_budget() {
let expected = serde_json::json!({
"schema_version": "loopx_runtime_identity_v1",
"package_version": "1.4.1", "release_id": null, "source_revision": null,
"package_fingerprint": format!("sha256:{}", "a".repeat(64)),
});
let payload = serde_json::json!({
"ok": true, "scope": "installation_only", "service_runtime_identity": expected,
});
let (_root, executable) =
installation_probe_fixture(&format!("sleep 1.2\nprintf '%s\\n' '{payload}'"));
assert_eq!(
runtime_identity_for_executable(executable.to_str().unwrap()),
Some(expected),
"slow, fully qualified wheels must not be treated as missing runtimes"
);
let mut banner = Command::new("sh");
banner.args(["-c", "sleep 2; printf 'Python 3.12.0\\n'"]);
assert!(
timed_output(banner).is_none(),
"version probes keep their short bound"
);
}

#[test]
#[cfg(unix)]
fn installation_probe_still_bounds_and_reaps_hung_qualification() {
let (root, executable) = installation_probe_fixture("echo $$ > \"$0.pid\"\nexec sleep 30");
let started = Instant::now();
assert!(runtime_identity_for_executable(executable.to_str().unwrap()).is_none());
assert!(
started.elapsed() < Duration::from_secs(13),
"qualification must remain bounded"
);
let pid = fs::read_to_string(root.path().join("bin/loopx.pid")).unwrap();
assert!(
!Command::new("kill")
.args(["-0", pid.trim()])
.stderr(Stdio::null())
.status()
.unwrap()
.success(),
"timed out probe must be reaped"
);
}

#[test]
fn status_readiness_is_explicit_and_fails_closed() {
assert_eq!(ServiceKind::Status.probe_path(), "/?readiness=1");
Expand Down
68 changes: 62 additions & 6 deletions apps/desktop/loopx-control-plane/src-tauri/src/update_backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,15 +623,71 @@ mod tests {
}

#[test]
#[cfg(target_os = "macos")]
#[ignore = "requires LOOPX_TEST_APP from a signed macOS build"]
fn real_backup_copy_retains_signature() {
fn real_release_backup_restores_damage_and_rejects_tampering() {
use std::os::unix::fs::MetadataExt;

let source = std::env::var("LOOPX_TEST_APP").unwrap();
let source = Path::new(&source);
let executable_name = declared_bundle_executable(source).unwrap();
let resource = "Contents/Resources/runtime/identity.json";
let identity = fs::read(source.join(resource)).unwrap();
let dir = tempfile::tempdir().unwrap();
let copied = dir.path().join("LoopX.app");
copy(Path::new(&source), &copied).unwrap();
assert!(copied
.join("Contents/Resources/runtime/identity.json")
.is_file());
let temporary_root = dir.path().canonicalize().unwrap();
let assert_temporary = |path: &Path| {
assert!(path.canonicalize().unwrap().starts_with(&temporary_root));
};
let previous = dir.path().join("previous");
let backup = previous.join("LoopX.app");
let target = dir.path().join("LoopX.app");
let executable = target.join("Contents/MacOS").join(executable_name);
let journal = dir.path().join("pending-rollback");
copy(source, &backup).unwrap();
copy(source, &target).unwrap();
assert!(installed_bundle_verifies(&executable));

// Only disposable copies are damaged. Exercise the same locator,
// verified copy and atomic replacement used by the recovery button
// with a shipped bundle, including its actual sealed runtime resource.
for damaged in [&executable, &target.join(resource), &target] {
assert_temporary(damaged);
if damaged.is_dir() {
fs::remove_dir_all(damaged).unwrap();
} else {
fs::remove_file(damaged).unwrap();
}
assert!(!installed_bundle_verifies(&executable));
restore_verified_backup(&executable, &previous, || {
// The journal callback must precede replacement: the damaged
// target must still be observable here.
assert!(!installed_bundle_verifies(&executable));
fs::write(&journal, "rollback").map_err(|error| error.to_string())
})
.unwrap();
assert_eq!(fs::read_to_string(&journal).unwrap(), "rollback");
assert!(installed_bundle_verifies(&executable));
assert_eq!(fs::read(target.join(resource)).unwrap(), identity);
assert!(signature_verifies(&backup));
fs::remove_file(&journal).unwrap();
}

// An invalid release backup must fail before journaling or swapping,
// leaving the exact installed directory and its signature intact.
let installed_inode = fs::metadata(&target).unwrap().ino();
assert_temporary(&backup.join(resource));
fs::write(backup.join(resource), "tampered").unwrap();
assert_eq!(
restore_verified_backup(&executable, &previous, || {
panic!("a tampered backup must never reach the commit callback")
}),
Err("backup_failed".into())
);
assert_eq!(fs::metadata(&target).unwrap().ino(), installed_inode);
assert!(installed_bundle_verifies(&executable));
assert!(!journal.exists());
assert_eq!(fs::read(source.join(resource)).unwrap(), identity);
assert!(signature_verifies(source));
}

#[test]
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/loopx-control-plane/static/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ const errors = {
app_install_failed: "App 安装未能完成,本次更新未生效;已确认当前版本完好且运行时可用,可直接重启继续使用,或重新检查更新后再试。",
app_install_incomplete: "App 安装中断,且无法确认当前版本是否完整,请勿直接重启。请在恢复与更新面板还原上一版本(或重新安装)后再试。",
runtime_pairing_required: "旧版本未能自动选择可用组件。请检查 App 更新,或通过恢复入口重新连接。",
backup_failed: "无法备份当前版本,更新已停止。请检查磁盘空间后重试。",
backup_unavailable: "没有可验证的 App 备份,未执行恢复。请重新安装完整 App;当前窗口仍可保留。",
backup_failed: "App 备份无法保存或未通过完整性校验,替换已停止。请检查磁盘空间与权限,或重新安装完整 App。",
rollback_failed: "恢复 App 未完成,请保持窗口打开。可重试恢复;若仍失败,请重新安装完整 App。",
};
function codeText(code, phase) {
if (runtimeExplicit && ["runtime_identity_unavailable", "runtime_selection_explicit"].includes(code)) {
Expand Down
Loading