Prevent duplicate desktop model sessions and record calibration evidence
This commit is contained in:
1 parent
8498c5401c
commit
fb8b45dcc9
7 files changed
+206
-10
No files matched your search
Generated
+18
@@ -3814,11 +3814,13 @@ dependencies = [
|
|||||||
"searchi-service",
|
"searchi-service",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2 0.10.9",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-single-instance",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"uuid",
|
"uuid",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
@@ -4586,6 +4588,22 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-single-instance"
|
||||||
|
version = "2.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.12.1"
|
version = "2.12.1"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ tauri-plugin-opener = "2"
|
|||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
searchi-core = { path = "../../../crates/searchi-core" }
|
searchi-core = { path = "../../../crates/searchi-core" }
|
||||||
searchi-service = { path = "../../../crates/searchi-service" }
|
searchi-service = { path = "../../../crates/searchi-service" }
|
||||||
@@ -30,6 +31,7 @@ searchi-media = { path = "../../../crates/searchi-media" }
|
|||||||
searchi-ai = { path = "../../../crates/searchi-ai" }
|
searchi-ai = { path = "../../../crates/searchi-ai" }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "bmp"] }
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp", "bmp"] }
|
||||||
|
tauri-plugin-single-instance = "2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
window-vibrancy = "=0.8.1"
|
window-vibrancy = "=0.8.1"
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//! Official single-instance transport, scoped to an explicitly selected profile.
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{ffi::OsString, io, path::PathBuf};
|
||||||
|
use tauri::{Emitter, Manager};
|
||||||
|
|
||||||
|
pub fn profile_identifier(base: &str, profile: Option<OsString>) -> io::Result<String> {
|
||||||
|
let Some(profile) = profile else {
|
||||||
|
return Ok(base.to_owned());
|
||||||
|
};
|
||||||
|
let path = crate::data_directory::resolve(PathBuf::new(), Some(profile))?;
|
||||||
|
// Resolve the existing ancestor without creating a first-use profile.
|
||||||
|
let canonical = crate::data_directory::canonical_exclusion(&path)?;
|
||||||
|
let mut digest = Sha256::new();
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
for unit in canonical.as_os_str().encode_wide() {
|
||||||
|
digest.update(unit.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
digest.update(canonical.as_os_str().as_bytes());
|
||||||
|
}
|
||||||
|
Ok(format!("{base}.profile{:x}", digest.finalize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_search(app: &tauri::AppHandle) {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let result = window
|
||||||
|
.show()
|
||||||
|
.and_then(|_| window.unminimize())
|
||||||
|
.and_then(|_| window.set_focus());
|
||||||
|
if let Err(error) = result {
|
||||||
|
eprintln!("Could not focus searchi: {error}");
|
||||||
|
}
|
||||||
|
let _ = window.emit("focus-search", ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn default_identity_is_unchanged_and_invalid_profile_rejected() {
|
||||||
|
assert_eq!(
|
||||||
|
profile_identifier("dev.searchi.desktop", None).unwrap(),
|
||||||
|
"dev.searchi.desktop"
|
||||||
|
);
|
||||||
|
assert!(profile_identifier("dev.searchi.desktop", Some("relative".into())).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn canonical_profiles_are_stable_distinct_and_do_not_expose_paths() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let first = root.path().join("Gemäß_日本_😀");
|
||||||
|
let second = root.path().join("other");
|
||||||
|
let id = profile_identifier("dev.searchi.desktop", Some(first.clone().into_os_string()))
|
||||||
|
.unwrap();
|
||||||
|
assert!(!first.exists());
|
||||||
|
std::fs::create_dir(&first).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
id,
|
||||||
|
profile_identifier(
|
||||||
|
"dev.searchi.desktop",
|
||||||
|
Some(first.join(".").into_os_string())
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
id,
|
||||||
|
profile_identifier("dev.searchi.desktop", Some(second.into_os_string())).unwrap()
|
||||||
|
);
|
||||||
|
assert!(!id.contains("Gemäß"));
|
||||||
|
assert_eq!(id.len(), "dev.searchi.desktop.profile".len() + 64);
|
||||||
|
assert!(!root.path().join("other").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ compile_error!("Release desktop builds require --features custom-protocol to emb
|
|||||||
mod ai_commands;
|
mod ai_commands;
|
||||||
mod appearance;
|
mod appearance;
|
||||||
mod data_directory;
|
mod data_directory;
|
||||||
|
mod instance;
|
||||||
mod model_download;
|
mod model_download;
|
||||||
mod semantic_commands;
|
mod semantic_commands;
|
||||||
mod settings;
|
mod settings;
|
||||||
@@ -545,16 +546,22 @@ fn start_folder_watching(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let mut context = tauri::generate_context!();
|
||||||
|
context.config_mut().identifier = instance::profile_identifier(
|
||||||
|
&context.config().identifier,
|
||||||
|
std::env::var_os("SEARCHI_DATA_DIR"),
|
||||||
|
)
|
||||||
|
.expect("searchi profile identity could not be resolved");
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
// Detect duplicates before any model, database, watcher or other plugin starts.
|
||||||
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||||
|
instance::focus_search(app);
|
||||||
|
}))
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().with_handler(|app, _, event| {
|
.plugin(tauri_plugin_global_shortcut::Builder::new().with_handler(|app, _, event| {
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
instance::focus_search(app);
|
||||||
let result = window.show().and_then(|_| window.unminimize()).and_then(|_| window.set_focus());
|
|
||||||
if let Err(error) = result { eprintln!("Could not focus Searchi: {error}"); }
|
|
||||||
let _ = window.emit("focus-search", ());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).build())
|
}).build())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
@@ -687,7 +694,7 @@ fn main() {
|
|||||||
hybrid_search,
|
hybrid_search,
|
||||||
hybrid_search_measured
|
hybrid_search_measured
|
||||||
])
|
])
|
||||||
.build(tauri::generate_context!())
|
.build(context)
|
||||||
.expect("Searchi desktop could not start")
|
.expect("Searchi desktop could not start")
|
||||||
.run(|app, event| {
|
.run(|app, event| {
|
||||||
if matches!(event, tauri::RunEvent::Exit) {
|
if matches!(event, tauri::RunEvent::Exit) {
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ an exploratory topic query, so these absence labels require concrete artifacts.
|
|||||||
The source inventory and blinded review have been completed. Independent AI
|
The source inventory and blinded review have been completed. Independent AI
|
||||||
review agreed with all expected file sets; the final labels are frozen in
|
review agreed with all expected file sets; the final labels are frozen in
|
||||||
`fixture.json` with hashes and a UTC timestamp in `fixture.lock.json`.
|
`fixture.json` with hashes and a UTC timestamp in `fixture.lock.json`.
|
||||||
No retrieval run, threshold selection or ranking change has occurred. Agent
|
The calibration-only baseline has run with genuine Google CPU inference.
|
||||||
agreement will not be described as human review.
|
No threshold selection or ranking change has occurred. Agent agreement will
|
||||||
|
not be described as human review.
|
||||||
|
|
||||||
The first independent agent review detected Windows-1252 mojibake in the
|
The first independent agent review detected Windows-1252 mojibake in the
|
||||||
authored German requests and ambiguity in the wildcard-comparator request.
|
authored German requests and ambiguity in the wildcard-comparator request.
|
||||||
@@ -46,3 +47,32 @@ why instead of altering this diagnostic silently.
|
|||||||
|
|
||||||
This small corpus does not close independent human relevance, media abstention,
|
This small corpus does not close independent human relevance, media abstention,
|
||||||
music discrimination, speech localisation or production-scale quality gates.
|
music discrimination, speech localisation or production-scale quality gates.
|
||||||
|
|
||||||
|
## Recorded CPU baseline, 2026-10-13
|
||||||
|
|
||||||
|
The shared-engine runner completed all 24 calibration requests plus six code
|
||||||
|
intent requests repeated under Code scope. Held-out requests were not evaluated.
|
||||||
|
All 14 frozen sources remained available as distractors.
|
||||||
|
|
||||||
|
| Scope | Positive requests | Lexical top-1 | Semantic / hybrid top-1 | Absent requests returning semantic / hybrid neighbours |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| All files | 16 | 1/16 | 16/16 | 8/8 |
|
||||||
|
| Additional Code scope requests | 4 | 0/4 | 4/4 | 2/2 |
|
||||||
|
|
||||||
|
The additional code rows repeat intents; they are not independent observations.
|
||||||
|
Lexical search returned no results for the absent requests. Semantic similarity
|
||||||
|
still retrieves related neighbours when the requested source is absent. This
|
||||||
|
baseline therefore supports useful positive retrieval, but does not establish
|
||||||
|
reliable abstention or justify a cutoff chosen without further calibration.
|
||||||
|
|
||||||
|
Ignored raw evidence: `.searchi/qa/calibration-v2-baseline.json`, SHA-256
|
||||||
|
`56f42c523527dd748918bb8f17be6bd7818158227e692fccaf387d915f9d0a94`.
|
||||||
|
The report records completion, calibration split and `held_out_evaluated: false`.
|
||||||
|
Frozen fixture SHA-256:
|
||||||
|
`000047fd17accd315b6138093bbdc9f353cd877b4bf1958fbe336e4a6b10cd2e`.
|
||||||
|
|
||||||
|
This ordered debug diagnostic used one CPU thread and the normal worker
|
||||||
|
`c01cb3ec13cfca722ee768e8350e1399a8b9a4d14a24a35adc3473a5cfc82a73`.
|
||||||
|
Lexical indexing took 19.9823 ms, semantic indexing 74,545.518 ms and retained
|
||||||
|
query-session startup 7,433.3914 ms. These are diagnostic setup measurements,
|
||||||
|
not native first-search latency or warm production p95.
|
||||||
+7
-2
@@ -1,6 +1,6 @@
|
|||||||
# Production readiness roadmap
|
# Production readiness roadmap
|
||||||
|
|
||||||
Status: implementation started; no production release yet. Windows VM tests use CPU only.
|
Status: initial Windows v0.1.0 published and installed; broader production gates remain open. Windows VM tests use CPU only.
|
||||||
|
|
||||||
Karl's release priority, 2026-10-13: publish a usable first Windows version and
|
Karl's release priority, 2026-10-13: publish a usable first Windows version and
|
||||||
install it on DEV-WIN11 now. Extensive platform/GPU, clean-machine, signing and
|
install it on DEV-WIN11 now. Extensive platform/GPU, clean-machine, signing and
|
||||||
@@ -43,6 +43,9 @@ release. Keep their evidence and unchecked production gates intact.
|
|||||||
see [incremental semantic design](INCREMENTAL-SEMANTIC.md).
|
see [incremental semantic design](INCREMENTAL-SEMANTIC.md).
|
||||||
|
|
||||||
## 3. Desktop
|
## 3. Desktop
|
||||||
|
- [x] Official single-instance transport with isolated explicit profiles;
|
||||||
|
Windows CPU duplicate launch exits without a second query worker.
|
||||||
|
See [launch evidence](SINGLE-INSTANCE.md); visual focus and other OS checks remain open.
|
||||||
- [x] Persisted solid/glass/frosted appearance and bounded background tint;
|
- [x] Persisted solid/glass/frosted appearance and bounded background tint;
|
||||||
opaque reading surfaces, serialized settings updates and high-contrast fallback.
|
opaque reading surfaces, serialized settings updates and high-contrast fallback.
|
||||||
- [ ] Verify native glass visually on Windows 11 22H2+; macOS/Linux currently
|
- [ ] Verify native glass visually on Windows 11 22H2+; macOS/Linux currently
|
||||||
@@ -250,7 +253,9 @@ Completion requires recorded evidence; targets are not measured results.
|
|||||||
after independent AI review; verify exact Git-preserved source/label bytes.
|
after independent AI review; verify exact Git-preserved source/label bytes.
|
||||||
- [x] Add a shared-engine calibration-only runner retaining raw cosine,
|
- [x] Add a shared-engine calibration-only runner retaining raw cosine,
|
||||||
lexical provenance, hybrid ranks and atomic incomplete evidence.
|
lexical provenance, hybrid ranks and atomic incomplete evidence.
|
||||||
- [ ] Execute calibration, freeze the proposed relevance policy, then
|
- [x] Execute the frozen calibration-only baseline with genuine Google CPU
|
||||||
|
inference; preserve raw results and leave held-out requests unevaluated.
|
||||||
|
- [ ] Freeze the proposed relevance policy, then
|
||||||
evaluate held-out queries without tuning against their results.
|
evaluate held-out queries without tuning against their results.
|
||||||
- [x] Explicit English/German code intent vetoes exclusive audio routing;
|
- [x] Explicit English/German code intent vetoes exclusive audio routing;
|
||||||
actual Google CPU regression preserves code among qualifying audio candidates.
|
actual Google CPU regression preserves code among qualifying audio candidates.
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Desktop launch behavior
|
||||||
|
|
||||||
|
Launching searchi again shows, restores and focuses the existing main window,
|
||||||
|
then focuses its search field through the existing `focus-search` event. It does
|
||||||
|
not interpret arguments, open supplied files, or start another index/model session.
|
||||||
|
|
||||||
|
The [official Tauri single-instance plugin](https://v2.tauri.app/plugin/single-instance/)
|
||||||
|
is registered first, before other plugins and application setup. Its native
|
||||||
|
transport provides duplicate detection; searchi does not maintain its own mutex.
|
||||||
|
|
||||||
|
Normal launches retain `dev.searchi.desktop`. An explicit absolute
|
||||||
|
`SEARCHI_DATA_DIR` creates an isolated desktop profile. Before plugin setup,
|
||||||
|
searchi resolves its canonical existing ancestor and any missing suffix without
|
||||||
|
creating the directory, then appends a SHA-256 of its native path
|
||||||
|
bytes to the runtime identifier. Windows uses UTF-16 units; Unix uses raw path
|
||||||
|
bytes. The identifier does not disclose the profile path. This also isolates
|
||||||
|
identifier-derived WebView/app-data locations. Same canonical profiles converge;
|
||||||
|
different profiles can run concurrently. This override is a developer/portable
|
||||||
|
profile mechanism, not a command-line search interface.
|
||||||
|
|
||||||
|
The plugin owns its Windows mutex, Linux D-Bus registration, or macOS socket.
|
||||||
|
Independent Linux D-Bus sessions have independent instances. Snap/Flatpak need
|
||||||
|
the upstream-documented D-Bus permissions; those packages remain unvalidated.
|
||||||
|
The previously published v0.1.0 application predates this change and cannot be
|
||||||
|
retroactively notified by the plugin.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Unit checks cover unchanged normal identity, rejected relative profiles,
|
||||||
|
Unicode profile paths, canonical aliases, stable/different identities and
|
||||||
|
absence of readable paths in identifiers.
|
||||||
|
- Native duplicate-launch behavior and window restoration require a running
|
||||||
|
desktop; platform evidence is recorded separately from unit checks.
|
||||||
|
- Windows VM inference tests must force CPU. No native macOS/Linux duplicate
|
||||||
|
launch or GPU claim is implied by the implementation.
|
||||||
|
|
||||||
|
### Windows VM evidence, 2026-10-13
|
||||||
|
|
||||||
|
- Desktop tests: 35 passed. Final strengthened profile tests: 2 passed.
|
||||||
|
Strict desktop all-target Clippy and formatting passed.
|
||||||
|
- Owned custom-protocol debug executable SHA-256
|
||||||
|
`93a22d5949fc0837f23e8455497b11333499bf41a5650b3f88493d5142de627f`.
|
||||||
|
- Fresh isolated profile A launched as PID 10624 using the normal bundled
|
||||||
|
worker SHA-256 `c01cb3ec13cfca722ee768e8350e1399a8b9a4d14a24a35adc3473a5cfc82a73`.
|
||||||
|
Its genuine Google CPU query worker PID 14520 remained identical before and
|
||||||
|
after duplicate PID 19240 exited successfully with code 0. The worker command
|
||||||
|
retained `--threads 1 --device cpu --role query`.
|
||||||
|
- Concurrent distinct profile B PID 12096 remained alive alongside profile A.
|
||||||
|
Profile B deliberately used an unavailable worker to avoid another inference
|
||||||
|
model; this proves desktop profile isolation, not semantic readiness there.
|
||||||
|
- Only owned test process trees were terminated afterward. Installed user app
|
||||||
|
PID 7716 remained alive and was never upgraded, focused or terminated by the
|
||||||
|
test. Raw local evidence: `.searchi/qa/single-instance/normal-result.json` and
|
||||||
|
`isolation-result.json` (ignored).
|
||||||
|
- This is process/lifecycle evidence. Native visual focus, restoration of a
|
||||||
|
minimized window and frontend search-field focus remain unverified.
|
||||||
Reference in new issue
Block a user