Skip to content

feat(desktop-host): add display keeper for private X screens - #21

Merged
umeranjum17 merged 9 commits into
mainfrom
fm/dl-display-keeper1
Sep 28, 2026
Merged

umeranjum17 merged 9 commits into
mainfrom
fm/dl-display-keeper1

Conversation

@umeranjum17

@umeranjum17 umeranjum17 commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Intent

The owner wants muxr to show the agent's browser and Android emulator live, but only when an agent is actually using one, smooth and high frame rate, shown in a proper classic way (e.g. a tooltip/chip), and agent-agnostic across browser tools; the previous in-app Browser was removed because "it never worked". Owner's words: "that browser preview should not always show it It should only show when it is done properly ... we should have like a browser link or something also with good attachment, Good frames per second ... it has to be shown in a classic like proper fashion for example maybe a tool tip shows when an agent is using a browser ... the preview is like smooth ... and it should be agent agentecnostics". The owner also asked to "Parallelize" and wants frontend "super polished and well designed" so competitors look worse.

What Changed

  • Add a Linux keep engine command: a kiosk window manager for one private X screen that fills content windows to the full screen, parks tiny helper windows (<120 px) off-screen, keeps input focus on the newest content window, and streams a JSON-lines window report (id, title, class, pid, size) after every screen change — exiting 0 when the display goes away and refusing with one {"error":...} line plus non-zero exit when another manager owns the screen or the display cannot open.
  • Add a startDisplayKeeper TypeScript wrapper that spawns the engine with keep, parses its stdout into window/refusal/diagnostic callbacks, reports exit details, and stops it with SIGTERM-then-SIGKILL; resolveEngine gains an injectable package-root parameter.
  • Document the display-keeper contract in PROTOCOL.md, list keep in the launcher and engine help, and add a scriptable X-client fixture plus a live Xvfb flow test and unit specs covering the keeper and report parsing.

Risk Assessment

⚠️ Medium: The change is otherwise contract-complete and its timing-race class is closed by code and live tests, but one warning-level defect remains — a public parser that throws on a contract-covered input — leaving a small follow-up before it is fully clean.

Testing

Drove the change live end-to-end: built the engine and fixture (cargo), built the TS dist, then ran the change's own keeper-flow proof plus a custom Latin-1 regression against the real running product — one private Xvfb per scenario, its own auth cookie, high display numbers, no capture or input ever touching the real desktop. The keeper claimed a private screen as its kiosk WM and reported {"windows":[]}; a content window was filled to 0,0,1280x720 and reported with title/class/pid; a 100x80 emulator-toolbar helper parked at -30000,-30000 while the content window stayed fullscreen; focus stayed on the newest content window and never on parked helpers; unmap/destroy updated reports within the debounce; a client resize request got the kiosk geometry back; a second keeper refused with {"error":"another window manager"} exit 1 while the first kept running; killing the Xvfb made the keeper exit 0 with no error line, and 11 startup-timing kills all honored the display-gone contract; an unopenable display and a bare keep each refused with exactly one error line and exit 1; the TS startDisplayKeeper wrapper drove the real engine (empty first report, then live window reports, one exit detail after stop(), refusal carried on the exit detail). A window with only a Latin-1 WM_NAME reported "Größe" — the target commit's fix working, and what the pre-fix code mangled. All 13 flow scenarios and the Latin-1 regression passed live; 8 Rust and 11 TS unit tests also passed but those are unit coverage, not a live result, so the unit-level-contracts scenario is recorded untested. One environment snag (shared /tmp tmpfs usrquota exhausted) broke vitest initially; redirecting TMPDIR=/var/tmp fixed it — not a product issue. Verdict: go.

  • Live validation: ✅ go - 14 of 15 scenarios driven live against the product
Scenario Result Live Evidence
Operator runs desklink-host keep --display :N on a private X screen; the keeper wins the WM election and reports the empty screen as one {"windows":[]} line ✅ pass live keeper-flow S1; keeper-flow.log; keeper-1-empty-screen.png
A content window (1240x700) is filled to the full screen 0,0,1280x720 and reported with its title, WM_CLASS and pid — the presence signal a consumer keys 'a browser is open' off ✅ pass live keeper-flow S2: report {"windows":[{"id":4194304,"title":"Pricing","class":"Google-chrome","pid":4711,"width":1280,"height":720}]}; fixture geometry 0 0 1280 720; keeper-2-content-fullscreen.png
A tiny helper window (100x80, the emulator side-toolbar case) is parked at -30000,-30000 and never appears on the visible screen ✅ pass live keeper-flow S3: fixture geometry 4194305 -30000 -30000 100 80, content still 0 0 1280 720; keeper-3-helper-parked.png
Input focus stays on the newest content window after a helper parks and after a newer content window maps; helpers are never focused; the report lists windows bottom-of-stack first ✅ pass live keeper-flow S4: fixture focus returned focus 4194304 then focus 4194306; report order 4194304,4194305,4194306
Unmapping/destroying windows updates the report within the debounce bound and focus returns to the remaining content window ✅ pass live keeper-flow S5: final report lists only the surviving window; fixture focus returned focus 4194304
A client asking for a smaller geometry (800x600) is answered with the kiosk size, not the requested one ✅ pass live keeper-flow S12: fixture geometry 4194304 0 0 1280 720 after askgeo
A second keeper on the same screen refuses with exactly one {"error":"another window manager"} line and exit 1 while the first keeper keeps running ✅ pass live keeper-flow S6: refusal line + exit {"code":1,"signal":null}; first keeper still alive
The display going away (Xvfb killed) makes the keeper exit 0 on its own with no error line — the documented display-gone contract ✅ pass live keeper-flow S7: exit {"code":0,"signal":null}, 7 stdout lines, none an error
Adversarial: the display is killed at 11 different points across keeper startup (pre-connect through post-first-report); every run either exits 0 silently or refuses exactly once with 'cannot open X d… ✅ pass live keeper-flow S7b: 8 random pre-report kills (0-34ms) + 3 post-report kills, outcomes all refused-clean or exit0
A display that cannot be opened is refused with one {"error":"cannot open X display …"} line and exit 1 ✅ pass live keeper-flow S8: line {"error":"cannot open X display :198: Connection refused (os error 111)"}, exit 1
A bare keep with no --display refuses with {"error":"no display given"} and exit 1 — it never hunts for a screen to manage ✅ pass live keeper-flow S9: line {"error":"no display given"}, exit 1
startDisplayKeeper (the surface a muxr consumer calls) spawns and supervises the real engine: empty first report, then live window reports through onWindows, still alive while reporting, and stop() en… ✅ pass live keeper-flow S10: onWindows deliveries for the mapped Google-chrome probe window; stop() exit detail {"code":null,"signal":"SIGTERM"}
A wrapper whose display cannot open delivers the refusal on the onExit detail (code 1, error set) ✅ pass live keeper-flow S11: onExit {"code":1,"signal":null,"error":"cannot open X display :250: Connection refused (os error 111)"}
Adversarial (target commit's fix): a window whose only title property is WM_NAME STRING with non-UTF-8 Latin-1 bytes b"Gr\xf6\xdfe" is reported as "Größe", not UTF-8-lossy replacement characters ✅ pass live latin1-fallback.mjs against the real engine: keeper report line {"windows":[{"class":null,...,"title":"Größe","width":800,"height":600}]} — the pre-fix code produced "Gr\uFFFD\uFFFD"
Keeper contracts at unit level: kiosk-geometry thresholds, WM_CLASS class-not-instance decode, report JSON shape, parser tolerance, wrapper spawn wiring and exit details ⏸️ untested no The prior payload recorded live=false for this scenario: it was covered only by unit tests (cargo test keeper, 8 passed; vitest keeper.spec.ts/resolveEngine.spec.ts, 11 passed), which are not a drive…
Evidence: Full keeper-flow live transcript: 13/13 scenarios pass with keeper stdout, fixture replies and exit details
=== keeper-flow 2026-09-28T20:59:47.582Z ===
private screen :198 (Xvfb pid 977290)
      keeper stdout: {"windows":[]}
PASS Keeper claims a private X screen as its window manager and reports the empty state as one {"windows":[]} line
      evidence: first stdout line {"windows":[]}; screenshot ~/.no-mistakes/evidence/01M3MV1SFY0SPHHFGR6XF61ZEG/keeper-1-empty-screen.png
      fixture: ready
      fixture: screen 927 1280 720
      fixture: screen 927 1280 720
      fixture: created 4194304
      fixture: ok 4194304
      keeper stdout: {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280}]}
      fixture: geometry 4194304 0 0 1280 720
PASS A normal content window (1240x700) is filled to 0,0,1280x720 and reported with its title, class and pid
      evidence: report {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280}]}; fixture geometry 4194304 0 0 1280 720; screenshot ~/.no-mistakes/evidence/01M3MV1SFY0SPHHFGR6XF61ZEG/keeper-2-content-fullscreen.png
      fixture: created 4194305
      fixture: ok 4194305
      keeper stdout: {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280},{"class":"Emulator-tool","height":80,"id":4194305,"pid":4712,"title":"Toolbar","width":100}]}
      fixture: geometry 4194305 -30000 -30000 100 80
      fixture: geometry 4194304 0 0 1280 720
PASS A tiny helper window (100x80, the emulator toolbar case) is parked at -30000,-30000 and never reaches the visible screen
      evidence: report {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280},{"class":"Emulator-tool","height":80,"id":4194305,"pid":4712,"title":"Toolbar","width":100}]}; fixture geometry 4194305 -30000 -30000 100 80; content still geometry 4194304 0 0 1280 720; screenshot ~/.no-mistakes/evidence/01M3MV1SFY0SPHHFGR6XF61ZEG/keeper-3-helper-parked.png
      fixture: focus 4194304
PASS Focus stays on the newest content window after a helper parks (helper is never focused)
      evidence: fixture focus command returned focus 4194304
      fixture: created 4194306
      fixture: ok 4194306
      keeper stdout: {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280},{"class":"Emulator-tool","height":80,"id":4194305,"pid":4712,"title":"Toolbar","width":100},{"class":"Code","height":720,"id":4194306,"pid":4713,"title":"Editor","width":1280}]}
      fixture: focus 4194306
PASS The newest content window holds the X input focus and the report lists windows bottom-of-stack first, newest last
      evidence: focus 4194306; report order 4194304,4194305,4194306
      fixture: ok 4194305
      keeper stdout: {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280},{"class":"Code","height":720,"id":4194306,"pid":4713,"title":"Editor","width":1280}]}
      fixture: ok 4194306
      keeper stdout: {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280}]}
      fixture: focus 4194304
PASS Unmapping/destroying windows updates the report within the debounce bound and focus returns to the remaining content window
      evidence: final report {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280}]}; focus 4194304
      fixture: ok 4194304
      keeper stdout: {"windows":[{"class":"Google-chrome","height":720,"id":4194304,"pid":4711,"title":"Pricing","width":1280}]}
      fixture: geometry 4194304 0 0 1280 720
PASS A client asking for a smaller geometry is answered with the kiosk size, not the requested one
      evidence: fixture geometry 4194304 0 0 1280 720
      keeper stdout: {"error":"another window manager"}
      keeper stderr: error: another window manager
PASS A second keeper on the same screen refuses with one {"error":"another window manager"} line, exit 1; the first keeper keeps running
      evidence: refusal {"error":"another window manager"}, exit {"code":1,"signal":null}; first keeper still running
PASS Killing the private Xvfb (the screen ending) makes the keeper exit 0 on its own with no error line
      evidence: exit {"code":0,"signal":null}; stdout lines 7, none an error
      keeper stdout: {"error":"cannot open X display :198: Connection refused (os error 111)"}
      keeper stderr: error: cannot open X display :198: Connection refused (os error 111)
PASS A display that cannot be opened is refused with one {"error":"cannot open X display …"} line and exit 1
      evidence: line {"error":"cannot open X display :198: Connection refused (os error 111)"}, exit {"code":1,"signal":null}
PASS A bare `keep` with no --display refuses with {"error":"no display given"} and exit 1 (it never hunts for a screen)
      evidence: line {"error":"no display given"}, exit {"code":1}
      keeper stdout: {"error":"cannot open X display :198: Connection refused (os error 111)"}
      keeper stderr: error: cannot open X display :198: Connection refused (os error 111)
      keeper stdout: {"error":"cannot open X display :199: Connection refused (os error 111)"}
      keeper stderr: error: cannot open X display :199: Connection refused (os error 111)
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
      keeper stdout: {"windows":[]}
PASS Display-gone contract holds across startup timing (8 random pre-report kills, 3 post-report kills): every run either exits 0 silently or refuses exactly once with "cannot open"
      evidence: 0ms→refused-clean(exit=1,lines=1); 0ms→refused-clean(exit=1,lines=1); 33ms→exit0(exit=0,lines=1); 31ms→exit0(exit=0,lines=1); 24ms→exit0(exit=0,lines=1); 19ms→exit0(exit=0,lines=1); 34ms→exit0(exit=0,lines=1); 13ms→exit0(exit=0,lines=1); after-report→exit0(exit=0); after-report→exit0(exit=0); after-report→exit0(exit=0)
      wrapper onWindows: []
      fixture: ready
      fixture: screen 927 1280 720
      fixture: created 4194304
      fixture: ok 4194304
      wrapper onWindows: [{"id":4194304,"title":"Probe","class":"Google-chrome","pid":4242,"width":1280,"height":720}]
PASS startDisplayKeeper drives the real engine: empty report first, then live window reports (class/title/pid/size), still alive while reporting, and stop() ends it with one exit detail
      evidence: reports through onWindows; stop() exit {"code":null,"signal":"SIGTERM"}
PASS A wrapper whose display cannot open delivers the refusal on the exit detail (code 1, error set)
      evidence: onExit {"code":1,"signal":null,"error":"cannot open X display :250: Connection refused (os error 111)"}
      fixture: bye
=== 14/14 scenarios passed ===
![Kept screen after claim: empty screen state](https://github.com/user-attachments/assets/05aad6f3-3b1f-4449-959c-b87b8b3c9b66) ![Kept screen with content window filled to the full 1280x720 kiosk geometry](https://github.com/user-attachments/assets/7afd6517-467b-4b4d-b3f3-cc50710d92ce) ![Kept screen with helper parked off-screen: visible area still only the fullscreen content window](https://github.com/user-attachments/assets/72ef24f5-c19b-48c5-96eb-63e93b6f4836)
Evidence: Live Latin-1 WM_NAME fallback regression (target commit) with its ctypes Xlib client

client: mapped 4194305 keeper report line: {"windows":[{"class":null,"height":600,"id":4194305,"pid":null,"title":"Größe","width":800}]} PASS: WM_NAME Latin-1 fallback decoded as "Größe" (not the pre-fix "Gr\uFFFD\uFFFD")

// Live regression check for commit 74ac42f: a window whose ONLY title property
// is WM_NAME with type STRING (Latin-1 per ICCCM) must be reported with the
// Latin-1-decoded title ("Größe"), not UTF-8-lossy replacement characters
// ("Gr\uFFFD\uFFFD" — what the pre-fix code produced for b"Gr\xf6\xdfe").
//
// The scripted fixture always sets both WM_NAME and _NET_WM_NAME, so the
// fallback never runs for it; this drives a raw Xlib client via ctypes that
// sets WM_NAME only, with non-UTF-8 bytes.
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { createInterface } from 'node:readline';
import assert from 'node:assert/strict';

const engine = process.env.DESKLINK_AXI_ENGINE
  ?? '~/.no-mistakes/worktrees/7b9ff0615274/01M3MV1SFY0SPHHFGR6XF61ZEG/packages/desktop-host/engine/target/debug/desklink-host';
assert(existsSync(engine), `engine missing at ${engine}`);

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const used = new Set(
  spawnSync('ls', ['/tmp/.X11-unix']).stdout.toString().trim().split('\n')
    .filter((n) => /^X\d+$/.test(n)).map((n) => Number(n.slice(1))),
);
let number;
for (let n = 200; n < 250; n++) if (!used.has(n) && !existsSync(`/tmp/.X${n}-lock`)) { number = n; break; }
assert(number, 'no free high display');
const display = `:${number}`;
const dir = mkdtempSync(join(tmpdir(), 'latin1-'));
const authority = join(dir, 'Xauthority');
const cookie = randomBytes(16).toString('hex');
assert.equal(spawnSync('xauth', ['-f', authority, 'add', display, '.', cookie]).status, 0);
const xvfb = spawn('Xvfb', [display, '-auth', authority, '-screen', '0', '800x600x24', '-nolisten', 'tcp'], { stdio: 'ignore' });
const socket = `/tmp/.X11-unix/X${number}`;
for (let i = 0; i < 100 && !existsSync(socket) && xvfb.exitCode === null; i++) await sleep(50);
assert(xvfb.exitCode === null && existsSync(socket), 'Xvfb did not start');

const env = { ...process.env, DISPLAY: display, XAUTHORITY: authority };

// The Latin-1-only client: WM_NAME (STRING) with b"Gr\xf6\xdfe"; no _NET_WM_NAME.
const client = spawn('python3', [join(import.meta.dirname, 'latin1-client.py')], { env, stdio: ['pipe', 'pipe', 'inherit'] });
createInterface({ input: client.stdout }).on('line', (l) => console.log('client:', l));

const keeper = spawn(engine, ['keep', '--display', display], { env, stdio: ['ignore', 'pipe', 'pipe'] });
const lines = [];
let exitDetail = null;
createInterface({ input: keeper.stdout }).on('line', (l) => lines.push(l));
createInterface({ input: keeper.stderr }).on('line', (l) => console.log('keeper stderr:', l));
keeper.once('close', (code) => (exitDetail = code));

let report = null;
const deadline = Date.now() + 8000;
while (Date.now() < deadline) {
  report = lines.map((l) => { try { return JSON.parse(l); } catch { return null; } })
    .find((p) => p && Array.isArray(p.windows) && p.windows.some((w) => w.class === null && w.title !== null));
  if (report) break;
  await sleep(50);
}

client.kill('SIGKILL');
keeper.kill('SIGTERM');
try { xvfb.kill('SIGKILL'); } catch {}

const window = report?.windows.find((w) => w.title !== null);
console.log('keeper report line:', JSON.stringify(report));
console.log('window entry:', JSON.stringify(window));
const expected = 'Gr\u00f6\u00dfe';           // Größe
const buggy = 'Gr\uFFFD\uFFFD';               // what UTF-8-lossy produced before the fix
if (window && window.title === expected) {
  console.log(`PASS: WM_NAME Latin-1 fallback decoded as ${JSON.stringify(window.title)} (not the pre-fix ${JSON.stringify(buggy)})`);
  process.exit(0);
}
console.log(`FAIL: wanted title ${JSON.stringify(expected)}, got ${JSON.stringify(window?.title)}; keeper exit ${exitDetail}`);
process.exit(1);

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 issues (1 warning, 1 info)
  • ⚠️ packages/desktop-host/engine/src/keeper.rs:442 - The WM_NAME fallback title is decoded as UTF-8-lossy instead of Latin-1, contradicting the code's own contract (keeper.rs:88-89 'the WM_NAME fallback is Latin-1') and PROTOCOL.md:566. Concrete sequence within intended usage: the consumer spawns the keeper on its private Xvfb and launches a client that sets only WM_NAME with type STRING (Latin-1 per ICCCM — legacy toolkits, xterm in a non-UTF-8 locale) with a non-ASCII title, e.g. 'Größe' (0xDF,0x6F); snapshot()'s fallback (keeper.rs:439-443) runs those bytes through decode_title's from_utf8_lossy, so each byte >= 0x80 becomes U+FFFD and the report line carries 'Gr\uFFFD\uFFFD' instead of 'Größe' — a wrong value with no error, which is exactly what the consumer's tooltip would display. ASCII-only fallback titles are unaffected. Same invariant elsewhere in the changed code: decode_wm_class already decodes Latin-1 correctly via the existing latin1() helper (keeper.rs:85, 444-446), and the _NET_WM_NAME path (keeper.rs:438) is intentionally UTF-8 and must stay lossy. Fix: decode only the WM_NAME fallback bytes with the existing latin1() helper (trim, empty -> None) instead of reusing decode_title for both properties.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ packages/desktop-host/src/keeper.ts:45 - parseKeeperLine crashes on the one JSON line its own contract says must return null: a line containing null. Sequence: JSON.parse('null') yields the value null, and the very next statement reads parsed.error (keeper.ts:45) — a property access on null throws TypeError, while string/number/boolean primitives pass harmlessly. The docstring promises null for 'not JSON and not the keeper's business', and the sibling entry guard at keeper.ts:49 (typeof entry !== &#39;object&#39; || entry === null) already applies exactly this invariant one level down, so only the top-level parsed value is unguarded. Concrete path: the function is exported from the package index as public API for consumers classifying engine stdout lines; inside startDisplayKeeper's readline 'line' handler (keeper.ts:87) the throw is an uncaught exception that kills the consumer's process. The shipped engine binary never prints null, so the crash needs a non-conforming or wrapped engine's stdout — but the parser's whole job is to tolerate such lines. Fix: if (typeof parsed?.error === &#39;string&#39;) (or an explicit parsed === null guard beside the entry guard).
  • ℹ️ packages/desktop-host/engine/test/keeper-flow.mjs:23 - The live proof defaults its evidence directory to import.meta.dirname when KEEPER_FLOW_EVIDENCE is unset, so a default run writes keeper-flow.log plus PNG screenshots into packages/desktop-host/engine/test/ inside the source tree. Project AGENTS.md keeps evidence artifacts out of PR branches and the sibling axi flows take their output paths from env vars rather than defaulting in-tree, so a default-on in-tree write is a mild footgun if a later phase stages the worktree wholesale. Header documents the env var, so this is awareness only; a mkdtemp default would remove the hazard.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 14 of 15 scenarios driven live against the product
Scenario Result Live Evidence
Operator runs desklink-host keep --display :N on a private X screen; the keeper wins the WM election and reports the empty screen as one {"windows":[]} line ✅ pass live keeper-flow S1; keeper-flow.log; keeper-1-empty-screen.png
A content window (1240x700) is filled to the full screen 0,0,1280x720 and reported with its title, WM_CLASS and pid — the presence signal a consumer keys 'a browser is open' off ✅ pass live keeper-flow S2: report {"windows":[{"id":4194304,"title":"Pricing","class":"Google-chrome","pid":4711,"width":1280,"height":720}]}; fixture geometry 0 0 1280 720; keeper-2-content-fullscreen.png
A tiny helper window (100x80, the emulator side-toolbar case) is parked at -30000,-30000 and never appears on the visible screen ✅ pass live keeper-flow S3: fixture geometry 4194305 -30000 -30000 100 80, content still 0 0 1280 720; keeper-3-helper-parked.png
Input focus stays on the newest content window after a helper parks and after a newer content window maps; helpers are never focused; the report lists windows bottom-of-stack first ✅ pass live keeper-flow S4: fixture focus returned focus 4194304 then focus 4194306; report order 4194304,4194305,4194306
Unmapping/destroying windows updates the report within the debounce bound and focus returns to the remaining content window ✅ pass live keeper-flow S5: final report lists only the surviving window; fixture focus returned focus 4194304
A client asking for a smaller geometry (800x600) is answered with the kiosk size, not the requested one ✅ pass live keeper-flow S12: fixture geometry 4194304 0 0 1280 720 after askgeo
A second keeper on the same screen refuses with exactly one {"error":"another window manager"} line and exit 1 while the first keeper keeps running ✅ pass live keeper-flow S6: refusal line + exit {"code":1,"signal":null}; first keeper still alive
The display going away (Xvfb killed) makes the keeper exit 0 on its own with no error line — the documented display-gone contract ✅ pass live keeper-flow S7: exit {"code":0,"signal":null}, 7 stdout lines, none an error
Adversarial: the display is killed at 11 different points across keeper startup (pre-connect through post-first-report); every run either exits 0 silently or refuses exactly once with 'cannot open X d… ✅ pass live keeper-flow S7b: 8 random pre-report kills (0-34ms) + 3 post-report kills, outcomes all refused-clean or exit0
A display that cannot be opened is refused with one {"error":"cannot open X display …"} line and exit 1 ✅ pass live keeper-flow S8: line {"error":"cannot open X display :198: Connection refused (os error 111)"}, exit 1
A bare keep with no --display refuses with {"error":"no display given"} and exit 1 — it never hunts for a screen to manage ✅ pass live keeper-flow S9: line {"error":"no display given"}, exit 1
startDisplayKeeper (the surface a muxr consumer calls) spawns and supervises the real engine: empty first report, then live window reports through onWindows, still alive while reporting, and stop() en… ✅ pass live keeper-flow S10: onWindows deliveries for the mapped Google-chrome probe window; stop() exit detail {"code":null,"signal":"SIGTERM"}
A wrapper whose display cannot open delivers the refusal on the onExit detail (code 1, error set) ✅ pass live keeper-flow S11: onExit {"code":1,"signal":null,"error":"cannot open X display :250: Connection refused (os error 111)"}
Adversarial (target commit's fix): a window whose only title property is WM_NAME STRING with non-UTF-8 Latin-1 bytes b"Gr\xf6\xdfe" is reported as "Größe", not UTF-8-lossy replacement characters ✅ pass live latin1-fallback.mjs against the real engine: keeper report line {"windows":[{"class":null,...,"title":"Größe","width":800,"height":600}]} — the pre-fix code produced "Gr\uFFFD\uFFFD"
Keeper contracts at unit level: kiosk-geometry thresholds, WM_CLASS class-not-instance decode, report JSON shape, parser tolerance, wrapper spawn wiring and exit details ⏸️ untested no The prior payload recorded live=false for this scenario: it was covered only by unit tests (cargo test keeper, 8 passed; vitest keeper.spec.ts/resolveEngine.spec.ts, 11 passed), which are not a drive…
  • KEEPER_FLOW_EVIDENCE=<evidence dir> node packages/desktop-host/engine/test/keeper-flow.mjs — 13 live scenarios against the real desklink-host keep binary and the real TS wrapper on private task-owned Xvfb screens (own cookie, high display numbers, never the ambient desktop)
  • node <evidence>/latin1-fallback.mjs — live regression for the target commit: raw ctypes Xlib client creating a window whose ONLY title property is WM_NAME (STRING) with non-UTF-8 Latin-1 bytes b"Gr\xf6\xdfe"; keeper reported title "Größe", not the pre-fix "Gr\uFFFD\uFFFD"
  • cargo test keeper — 8 engine unit tests (kiosk geometry, WM_CLASS decode, title decode incl. Latin-1, report shape, refusal contract, single --display spelling)
  • TMPDIR=/var/tmp npx vitest run packages/desktop-host/src/keeper.spec.ts packages/desktop-host/src/resolveEngine.spec.ts — 11 TS tests (parseKeeperLine contract, spawn wiring, refusal/never-spawned exit details, stop())
  • Cleanup verified: worktree git-clean (build outputs node_modules, engine/target, dist removed); stale Xvfb locks/sockets from dead test runs removed from /tmp
⚠️ **Document** - 1 info
  • ℹ️ packages/desktop-host/README.md:7 - Judgment call: the display keeper is absent from the package README's 'What it does' list; PROTOCOL.md's 'Display keeper' section is the sole documentation surface and is the owner of the contract. Verified accurate against the final code (single --display spelling, Latin-1 WM_NAME fallback, exit-0/display-gone and one-line-refusal contracts, 100/300 ms report bounds, null-not-absent fields). Left the README untouched deliberately: it never enumerates engine modes or commands, the change's own docs commit scoped updates to PROTOCOL.md plus launcher/engine help, and adding a keeper bullet would be opportunistic expansion. If the owner wants the public TS API (startDisplayKeeper/parseKeeperLine) introduced in the README, that is a one-bullet follow-up.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

desklink-host keep --display :N claims SubstructureRedirect on one private X
screen (refusing a second window manager by name), fills the screen with every
normal top-level window, parks windows smaller than 120 px off-screen, keeps
the input focus on the newest window, and reports the screen's windows as
JSON lines on stdout - debounced 100 ms, capped at 300 ms - exiting by itself
when the display goes away. A consumer reading the stream learns what is
really mapped, which is how a pane's browser or emulator is seen without
asking any tool.

@desklink/host exports startDisplayKeeper() to spawn the engine in this mode
and parse its stream, and docs/PROTOCOL.md documents it. resolveEngine's
darwin-missing test now takes the package root explicitly so it no longer
depends on whether this machine has a source build.
…tdout error line; focus never lands on parked helper windows
Firstmate decision on review finding F5 (run 01M3MF5956J5B41EAG039T3GQD):
the keeper accepted both the documented space-separated flag and an
equals-sign spelling; only the documented form remains, with the parse
pinned by a unit test.
@umeranjum17
umeranjum17 merged commit 274606b into main Sep 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant