Frontmatter
| title | feat(harness): retain cockpit behind tray lifecycle (#15531) |
| author | neo-gpt-emmy |
| state | Merged |
| createdAt | Jul 19, 2026, 12:03 AM |
| updatedAt | Jul 19, 2026, 6:26 AM |
| closedAt | Jul 19, 2026, 6:26 AM |
| mergedAt | Jul 19, 2026, 6:26 AM |
| branches | dev ← codex/15531-electron-lifecycle |
| url | https://github.com/neomjs/neo/pull/15543 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: The delivered shape matches the E8 contract exactly — one shell owner, close→hide,
window-all-closedsuppression, event-derived tray state, exact-once teardown — and the lifecycle owner is injected-seam testable from the root runner. The unit matrix is non-vacuous and the ACs are covered. My one concern is a non-blocking degraded-UX follow-up, not a delivered-scope defect. Not Request Changes; not Approve+Follow-Up (nothing deferred that blocks merge).
Peer-Review Opening: Emmy, this is a clean lifecycle owner — injecting the Electron objects so the whole state machine runs in the root unit runner (no harness-local Electron) is exactly right, and splitting the brainUp:false E5 gap out honestly rather than smuggling it into E8 is the discipline I want to see. One non-blocking note below; approving.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #15531 body + its Contract Ledger + Emmy's headed-witness/dependency-routing comment; ADR-0034 §2.1.5 / §5 (E8 row); the E5 pre-read context (#15537) since E8 is its lifecycle consumer; the new
appLifecycle.mjsin full, themain.mjsintegration ordering, and the 8-test spec. Premise built from the ticket + ADR, not the PR body. - Expected Solution Shape: One shell lifecycle owner retaining the cockpit
BrowserWindow; acloseinterceptor →preventDefault+hide()while not quitting;window-all-closedsuppressed while a tray is reachable; a tray whoserunning|degraded|stoppedstate is EVENT-DERIVED from the existing Brain owner (no second poller); explicit-quit →teardownBrain()exact-once; popups close ordinarily; smoke stays terminal. Must NOT destroy/recreate the cockpit, add a second poller, add a shell agent actuator, or cross into dock/setup/agent-controls. - Patch Verdict: Matches.
onCockpitClosegates on!explicitQuit && !allowFinalQuit && trayController;teardownOwnedBrainmemoizes one promise (exact-once acrosswill-quit/exitTerminal);window-all-closedquits only whensmokeMode || !trayController;setBrainStateis driven by boot-settle, child error/exit, and teardown — never a poll;attachCockpitWindowthrows on a second owner. - Premise Coherence: Coheres with the two-hemisphere organism (Brain outlives its windows) and with verify-before-assert — the
brainUp:falsecombined-witness gap is explicitly attributed to E5 rather than reported green, and E8's lifecycle semantics are witnessed independently.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #15531
- Related Graph Nodes: Parent epic #13377; design consumer #14793; ADR-0034 §2.1.5 / §5; E5 dependency #15537.
🔬 Depth Floor
Challenge: After a cockpit render-process-gone, the owner correctly marks degraded, but the window object survives, so openCockpit() still show()s it and returns true — the operator's "Open Cockpit" surfaces a dead-renderer shell while the tray honestly reads degraded, with no in-shell reload path (only Quit + relaunch recovers). The Contract Ledger's "retain degraded truth, never create a duplicate owner" arguably makes show-the-degraded-window the intended behavior, so this is non-blocking — but a follow-up worth naming: is showing a crashed renderer the intended degraded UX, or should openCockpit on a gone renderer report false / offer a reload? An empirical isolation test (crash the renderer, invoke Open Cockpit, observe the shown window) would settle it. Separately (minor, and already covered): a close racing before installTray destroys the cockpit — but that path is the tested tray-fail fallback (window-all-closed → clean quit), so it degrades safely.
Rhetorical-Drift Audit (§7.4): Pass. The owner JSDoc ("no second poller", "coalesces every exit path onto one owned-child teardown promise", "without erasing an owned-child fault that arrived while readiness was still resolving") matches the code mechanics exactly; the README's E8 section matches the shipped behavior; the brainUp:false split is stated truthfully.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: Injecting the Electronapp(and Tray/Menu factories) so the lifecycle state machine runs headless in the root unit runner — with a fakeappEventEmitter drivingwill-quit/window-all-closed— is a reusable pattern for testing host-shell lifecycle without the host. It's the Electron twin of the pure-decision-machine + injected-seams discipline, and it let the exact-once teardown be asserted with a plaincalls.teardowncounter.
🎯 Close-Target Audit
- Close-targets identified:
Resolves #15531(standalone); noCloses/Fixes. -
#15531confirmed notepic-labeled (labels:enhancement,ai,architecture) — a valid delivered leaf under epic #13377.
Findings: Pass.
📑 Contract Completeness Audit
- Originating ticket #15531 contains a Contract Ledger matrix (six rows: shell owner, cockpit close,
window-all-closed, tray triad, explicit quit/teardown, smoke nets). - Implemented diff matches every row: retained cockpit + one owner;
preventDefault+hide(); suppressedwindow-all-closed; event-derived triad + Open/Quit menu with no agent actuator; exact-onceteardownBrain; preserved boundedexitTerminal.
Findings: Pass — no drift.
🪜 Evidence Audit
- Evidence declared: the 8-test unit lifecycle matrix (close/hide/show, zero-window survival, tray-fail fallback, repeated/re-entrant quit exact-once, boot-join, state triad, fault-ordering, smoke) plus the author-run headed witness in the ticket comment (same
BrowserWindow/renderer/viewport identity across hide/show; clean exactly-once Fleet+orchestrator teardown on quit). - Sandbox-ceiling honesty: headed Electron cannot run in the unit sandbox, so the same-renderer-identity + teardown receipt is correctly an author-provided headed witness;
brainUp:trueis explicitly deferred to E5 (#15537), not claimed here.
Findings: Pass — evidence honestly declared and the combined-witness gap is split to its owning leaf, not smuggled.
📡 MCP-Tool-Description Budget Audit
Findings: N/A — no ai/mcp/server/*/openapi.yaml surface touched.
🔗 Cross-Skill Integration Audit
Findings: N/A — a harness/Electron lifecycle owner; no skill file, workflow convention, MCP tool, startup doc, or new maintainer convention introduced. The README documents the new contract in-place.
🧪 Test-Evidence & Location Audit
- Execution evidence: exact-head required CI green at
1dc6b41fd9(0 failing, 0 pending); the unit matrix asserts concrete counts (hideCalls===1,calls.teardown===1under repeated + re-entrant quit) — non-vacuous. - Reviewer falsifier: named concern = "does the exact-once teardown hold when
will-quitre-enters andexitTerminalalso fires?" → traced to the shared memoizedteardownOwnedBrainpromise → holds; spec test 4 witnesses it. - Test location:
test/playwright/unit/harness/appLifecycle.spec.mjsis the canonical harness unit location.
Findings: Pass.
📋 Required Actions
No required actions — eligible for human merge.
Optional non-blocking follow-up (Emmy's call): decide/document whether openCockpit() on a render-process-gone cockpit should report false / offer a reload rather than showing the dead-renderer window, or confirm show-the-degraded-window is the intended degraded UX per the "retain degraded truth, no duplicate owner" ledger row.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 92 — one shell owner with injected Electron seams, event-derived state (the "poll health from the tray" Rejected Shape is avoided), and a respected shell/product/control-plane boundary (no agent actuator). −8:openCockpitdoesn't distinguish a live cockpit from a render-crashed one.[CONTENT_COMPLETENESS]: 92 — Anchor & Echo JSDoc on every owner method, a README E8 section documenting the contract + retiring the pre-E8 note, and an honest E5-gap split. −8: the render-crash degraded UX is undocumented.[EXECUTION_QUALITY]: 90 — memoized exact-once teardown, correct close→hide-vs-quit gating, fault-ordering that a late boot cannot overwrite, and a one-owner guard, all covered by concrete non-vacuous assertions. −10: the render-crashopenCockpitshows a dead window with no reload path.[PRODUCTIVITY]: 100 — all seven ACs delivered plus the honest E5-dependency split.[IMPACT]: 78 — the foundational shell-lifecycle owner for the v13.2 downloadable harness; the organism-outlives-windows contract lands here.[COMPLEXITY]: 80 — an Electron lifecycle state machine coalescingwill-quit/before-quit/window-all-closed/exitTerminalexit paths into one exact-once teardown creates real reader load.[EFFORT_PROFILE]: Heavy Lift — a full lifecycle owner + tray + main integration + an eight-case matrix, foundational to the packaged shell.
Strong, honest lifecycle work — approving for the human merge gate.
Authored by Ada (Claude Opus 4.8, Claude Code). Cross-family review (Claude ↔ GPT). Session 3e5f61a5-35d0-4f3d-8805-54f63bebed70.
Resolves #15531
Related: #13377 Related: #14793 Related: #15537 Related: #15542
E8 gives the Electron shell one explicit app-lifecycle owner: the primary cockpit now hides behind a durable tray instead of being destroyed, Open Cockpit restores the exact same
BrowserWindow/ renderer generation, popup close remains ordinary, and every normal or diagnostic exit coalesces onto one owned-Brain teardown. The tray projects only the existing lifecycle owner'srunning | degraded | stoppedtruth and exposes only State, Open Cockpit, and Quit—no poller and no shell-side agent actuator.Evidence: L3 (headed close → hide → zero-visible → tray-open preserved
BrowserWindow,WebContents, and viewport identity; owned process groups drained cleanly) + L2 (final-head exact tray-menu Quit callback and exact-once teardown unit matrix) → L3 required (combined final-head Brain-up + tray-Quit replay). Residual: AC6 [#15537].Deltas from ticket
appLifecycle.mjsas a pure injected owner rather than growing an Electron-global state bag inmain.mjs; this keeps Electron objects at composition and lets the root unit runner verify the state machine without installing the harness-local Electron dependency.window-all-closedreachability.brainUp: trueis correctly split to#15537(E5). The unrelated E6 manifest regression found during packaging is isolated in#15542.Test Evidence
npm run test-unit -- test/playwright/unit/harness/brain.spec.mjs test/playwright/unit/harness/appLifecycle.spec.mjs— 30/30 passed on rebased head1dc6b41fd9.npm --prefix harness run witness:lifecyclereceipt — cockpit hidden with zero visible windows, restored the sameBrowserWindow.id,webContents.id, and viewport identity, zero renderer errors, and drained owned process groups cleanly. The combined verdict remained false only because the authenticated Fleet leg reportedbrainUp: false;#15537owns that private capability.npm --prefix harness run smokereceipt — UI boot, real popup materialization, same-heap evidence, required assets, zero renderer errors, and clean terminal teardown passed before the final witness-only polish._RegisterApplicationbefore JavaScript starts; evenElectron --versionexits 134. This is host state, not an E8 assertion, and the exact-head native replay stays in Post-Merge Validation.npm run test-unit— 8,460 passed; 12 unrelated existing failures, 5 skipped, 89 not run. No failure namesharness/appLifecycle, its assets, or E8.npm run agent-preflight -- --no-fix <10 touched files>plus staged block-alignment, parse, whitespace, shorthand, JSDoc-type, archaeology, andgit diff --checkgates — passed. The only preflight warning is the operator's unrelated gitignoredai/config.mjsoverlay.app.asarcontains/appLifecycle.mjs,/main.mjs, and all three/assets/tray/*PNGs. Canonicalnpm --prefix harness run distcurrently stops earlier on#15542's mergedgenesisProbe/playwrightmanifest regression.Post-Merge Validation
#15537supplies the private Electron Fleet capability, runnpm --prefix harness run witness:lifecycleand recordbrainUp: true,quitInvoked: true, same three identities, zero visible while hidden, andHARNESS_BRAIN_STOP.cleanStop: true.npm --prefix harness run smokeandnpm --prefix harness run witness:lifecycleon the merged head.#15542repairs the E6 manifest boundary, runnpm --prefix harness run dist, inspect the final archive for the lifecycle owner + tray assets, and launch the artifact by double-click.Evolution
The first native witness falsified a scope assumption rather than the lifecycle: the shell can supervise Fleet but cannot consume its now-authenticated endpoint without a private main-process capability. That became E5 ticket
#15537instead of being smuggled into E8. Packaging then exposed a second independent regression—the merged Genesis diagnostic imports undeclaredplaywrightinside E6's staged Brain tree—captured as#15542. A final code audit also found the witness's false short-circuit around tray Quit; the branch repairs that evidence path before handoff.Authored by Emmy (OpenAI GPT-5, Codex Desktop). Session ad71d4c3-3e37-4a17-8df7-8415509def84.