Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.
App launcher shows duplicated apps that accumulate over the session — mergeAppRows is not idempotent
Reported by Chispes · filed Jul 24, 2026Closed upstream on 2026-07-24 by its own reporter Chispes, as not planned. Closed by its own reporter — not by itself evidence of a fix.
At a glance
- 1 · What is claimed?
- Only the report text below — no validator has restated it as checkable claims yet.
- 2 · What does it affect?
- Not yet mapped to any requirement or component.
- 3 · Is it reproducible?
- No validation has been run yet.
- 4 · What remains unclear?
- Not validated yet: no claims, no reproducer.
- 5 · What happens next?
- Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.
Original report
Summary
On Omarchy 4 (quattro), apps appear duplicated in the Quickshell "Apps…" launcher, and the duplicates grow over the life of the session (e.g. YouTube listed 6–7×, Discord 3×; other apps affected too). The underlying .desktop files are not duplicated — there is a single file and a single Name= for each app on disk. Restarting the shell (omarchy-restart-shell) makes every app show exactly once again, which confirms the duplication is in-memory state that accumulates in the long-running shell process, not something on disk.
Steps to observe / confirm
- Use Omarchy 4 for a while (the shell is a single long-running process). Web apps / newly installed apps make it more visible.
- Open the app launcher and search e.g.
disc. An affected app (e.g. Discord) shows multiple identical rows. - Verify it is not on disk:
$ ls ~/.local/share/applications/Discord.desktop # one file
$ grep -rh '^Name=Discord' ~/.local/share/applications /usr/share/applications | sort | uniq -c
1 Name=Discordomarchy-restart-shell→ reopen the launcher → every app shows once again. Duplicates re-accumulate later.
What is verified, and what is still open
Verified: items 1–4 above, and the code-level defect described below (read directly from the shipped Menu.qml).
Still open: the exact event that first introduces a duplicate id into itemOrder. omarchy-refresh-applications does not trigger it (tested: apps stayed at 1×). I am still investigating and will update this issue when I pin it down.
The defect below is independent of that trigger: once a duplicate id exists, mergeAppRows cannot heal it, which is what makes duplicates permanent and cumulative rather than transient.
The defect: mergeAppRows is not idempotent
shell/plugins/menu/Menu.qml, function `mergeAppRows()` (lines 246–291). Two problems:
1. Cleanup misses already-duplicated ids (lines 249–255) — an id is only removed from root.items the first time it is seen in itemOrder:
var nextOrder = []
for (var i = 0; i < root.itemOrder.length; i++) {
var id = root.itemOrder[i]
var existing = root.items[id]
if (existing && existing.kind === "app") delete root.items[id] // deletes on 1st occurrence
else nextOrder.push(id) // 2nd occurrence: items[id] is now undefined -> orphan is KEPT
}If itemOrder contains an app id twice, the first occurrence deletes root.items[id]; the second occurrence then looks it up, finds undefined, falls into the else, and preserves the duplicate in nextOrder. The same happens to any apps.* id whose root.items entry no longer exists for any other reason.
2. Unconditional re-add (line 286) — the rebuild loop pushes every row with no "already present" guard:
nextOrder.push(itemId) // line 286 — always pushes, even if itemId is already in nextOrderThe sibling function mergeProviderRows does have exactly the guard that is missing here — line 328: `if (!root.items[id]) nextOrder.push(id)`.
Net effect: if itemOrder ever ends up holding the same app id twice — or holding an apps.* id with no matching root.items entry — mergeAppRows can never recover: the stale id is preserved by the cleanup loop and a fresh copy is pushed by the re-add loop. Because the shell is a single long-running process, the duplicate then persists and accumulates for the rest of the session. This matches the observed behavior (single file on disk; count grows over the session; cleared by omarchy-restart-shell).
Proposed fix (shell/plugins/menu/Menu.qml, mergeAppRows)
Make it dedup-safe, so it is idempotent regardless of what state itemOrder is in:
- Robust cleanup — drop every prior app id regardless of whether
root.items[id]still exists:
for (var i = 0; i < root.itemOrder.length; i++) {
var id = root.itemOrder[i]
if (id.indexOf("apps.") === 0) { delete root.items[id]; continue } // strip all app rows
nextOrder.push(id)
}- Guard the re-add (mirroring line 328), so a duplicate id can't create a second row:
if (nextOrder.indexOf(itemId) === -1) nextOrder.push(itemId)Optionally also dedup by entry.id in shell/services/AppSearch.js sortedEntries (lines 97–122).
Workaround (user side)
omarchy-restart-shell clears the duplicates until they re-accumulate.
Environment
- Omarchy
4.0.0.r1278.g2c8639f-1(quattro / 4.0.0 alpha), commit2c8639f - Quickshell
0.3.0(quickshell-git 0.3.0.r18.g10b439f-2) - Hyprland
0.56.0
View raw text
## Summary
On Omarchy 4 (`quattro`), apps appear **duplicated** in the Quickshell "Apps…" launcher, and the duplicates **grow over the life of the session** (e.g. YouTube listed 6–7×, Discord 3×; other apps affected too). The underlying `.desktop` files are **not** duplicated — there is a single file and a single `Name=` for each app on disk. Restarting the shell (`omarchy-restart-shell`) makes every app show exactly once again, which confirms the duplication is in-memory state that accumulates in the long-running shell process, not something on disk.
## Steps to observe / confirm
1. Use Omarchy 4 for a while (the shell is a single long-running process). Web apps / newly installed apps make it more visible.
2. Open the app launcher and search e.g. `disc`. An affected app (e.g. Discord) shows multiple identical rows.
3. Verify it is **not** on disk:
```
$ ls ~/.local/share/applications/Discord.desktop # one file
$ grep -rh '^Name=Discord' ~/.local/share/applications /usr/share/applications | sort | uniq -c
1 Name=Discord
```
4. `omarchy-restart-shell` → reopen the launcher → every app shows **once** again. Duplicates re-accumulate later.
## What is verified, and what is still open
**Verified:** items 1–4 above, and the code-level defect described below (read directly from the shipped `Menu.qml`).
**Still open:** the exact event that first introduces a duplicate id into `itemOrder`. `omarchy-refresh-applications` does **not** trigger it (tested: apps stayed at 1×). I am still investigating and will update this issue when I pin it down.
The defect below is independent of that trigger: once a duplicate id exists, `mergeAppRows` **cannot heal it**, which is what makes duplicates permanent and cumulative rather than transient.
## The defect: `mergeAppRows` is not idempotent
`shell/plugins/menu/Menu.qml`, function **`mergeAppRows()`** (lines 246–291). Two problems:
**1. Cleanup misses already-duplicated ids (lines 249–255)** — an id is only removed from `root.items` the *first* time it is seen in `itemOrder`:
```js
var nextOrder = []
for (var i = 0; i < root.itemOrder.length; i++) {
var id = root.itemOrder[i]
var existing = root.items[id]
if (existing && existing.kind === "app") delete root.items[id] // deletes on 1st occurrence
else nextOrder.push(id) // 2nd occurrence: items[id] is now undefined -> orphan is KEPT
}
```
If `itemOrder` contains an app id twice, the first occurrence deletes `root.items[id]`; the second occurrence then looks it up, finds `undefined`, falls into the `else`, and **preserves the duplicate** in `nextOrder`. The same happens to any `apps.*` id whose `root.items` entry no longer exists for any other reason.
**2. Unconditional re-add (line 286)** — the rebuild loop pushes every row with no "already present" guard:
```js
nextOrder.push(itemId) // line 286 — always pushes, even if itemId is already in nextOrder
```
The sibling function `mergeProviderRows` does have exactly the guard that is missing here — **line 328: `if (!root.items[id]) nextOrder.push(id)`**.
**Net effect:** if `itemOrder` ever ends up holding the same app id twice — or holding an `apps.*` id with no matching `root.items` entry — `mergeAppRows` can never recover: the stale id is preserved by the cleanup loop and a fresh copy is pushed by the re-add loop. Because the shell is a single long-running process, the duplicate then persists and accumulates for the rest of the session. This matches the observed behavior (single file on disk; count grows over the session; cleared by `omarchy-restart-shell`).
## Proposed fix (`shell/plugins/menu/Menu.qml`, `mergeAppRows`)
Make it dedup-safe, so it is idempotent regardless of what state `itemOrder` is in:
- **Robust cleanup** — drop every prior app id regardless of whether `root.items[id]` still exists:
```js
for (var i = 0; i < root.itemOrder.length; i++) {
var id = root.itemOrder[i]
if (id.indexOf("apps.") === 0) { delete root.items[id]; continue } // strip all app rows
nextOrder.push(id)
}
```
- **Guard the re-add** (mirroring line 328), so a duplicate id can't create a second row:
```js
if (nextOrder.indexOf(itemId) === -1) nextOrder.push(itemId)
```
Optionally also dedup by `entry.id` in `shell/services/AppSearch.js` `sortedEntries` (lines 97–122).
## Workaround (user side)
`omarchy-restart-shell` clears the duplicates until they re-accumulate.
## Environment
- Omarchy `4.0.0.r1278.g2c8639f-1` (quattro / 4.0.0 alpha), commit `2c8639f`
- Quickshell `0.3.0` (`quickshell-git 0.3.0.r18.g10b439f-2`)
- Hyprland `0.56.0`
Validation
Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.
Linked
Not linked to any known issue, other report or change yet.
Decision
Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.
No decision has been recorded yet.