--- title: "The Dialog Opened in 7.2 Seconds. My Script Waited 5." description: "A cron job kept reporting 'the composer dialog did not open'. The three DOM stages mounted at 1.8 s, 2.8 s and 7.2 s — and the script only looked once, at 5 s." pubDate: 2026-10-15 category: ai tags: ["automation", "linkedin", "cron", "browser", "agents"] ogImage: /og/the-dialog-opened-in-7-seconds-my-script-waited-5.png banner: /banners/the-dialog-opened-in-7-seconds-my-script-waited-5.png draft: true --- ## The failure message that was technically true and completely wrong My scheduled posting job reports to my phone when it can't do its job, and that morning it said: ```text ⚠️ Cron 'linkedin-daily-post' failed: Script exited with code 1 LinkedIn daily post — FAILED FAILED: the composer dialog did not open ``` Confident, specific, and useless. Nothing had been published, the site was up, the browser profile was logged in, and the message told me which component to blame: a dialog that never appeared. The dialog had appeared. It appeared 3.2 seconds before my script decided it hadn't. ## Where I looked first, and why that was the wrong place An automation that fails against a website has three usual suspects: the session expired, the site changed its markup, or the account got rate-limited. I checked the session first — it was healthy. And in my own project notes, the two earlier failures, four and five days before, carried the **same** message, with the working theory written down as "the session may be dead". That sentence is the tell. Two identical failures, one theory, and the theory was never confirmed. A repeated failure with an unconfirmed cause is a measurement problem, not a diagnosis problem. ## Measure the mount instead of the message So I opened the composer with the browser in the state the script leaves it, and timed when each piece of the DOM actually arrives: ```text +1.8 s the composer dialog element exists +2.8 s the Post / Proofread buttons exist +7.2 s the text editor exists <- the node the script actually types into ``` The script clicked "start a post", slept five seconds, and looked **once**. At five seconds the dialog was there, the buttons were there, and the editor was not — and the code's single failure message covered all three cases with the sentence "the dialog did not open". Nothing was broken. The script was asking a question at the wrong time and reporting the answer as a fact about LinkedIn. ## The fix: poll for the node you need, not for the clock - **Poll for the specific node**, with a 40-second deadline, instead of sleeping a fixed amount and checking once. - **Name the missing precondition.** A timeout now says whether the dialog, the buttons or the editor failed to appear. That single change would have saved two sessions. - **Retry the click once if no dialog appears at all.** A swallowed click is a real second failure mode, and it deserves a different response than a slow mount. - **Leave a screenshot on failure.** The next person debugging this gets pixels, not a sentence. - **Remove the same single-shot look from the edit-and-verify modal**, which had the identical shape on a different screen — the same bug in a second place is not a coincidence, it is a pattern in how the code waits. - **Tests:** 13 new browser-free tests pin the staged mount and the swallowed click, so a future refactor that reintroduces `sleep(5)` fails in CI instead of on my phone. ## The second defect, which only publishing could reveal With the race fixed, the post went out — but not on the first attempt, and the second failure is the more interesting one. The first click on "Post" was **ignored for 75 seconds**. Then an identical click — same coordinates, same DOM, and I verified the pointer sequence did reach the button's span — was accepted in **2 seconds**. A click that is delivered and does nothing looks exactly like a click that was never delivered. The only way to tell them apart is to stop treating the click as the proof and make the *result* the proof: - the composer closing is now the only accepted confirmation of submission; - a bounded three-attempt re-click, with a refusal to re-click a disabled button; - and no tearing the tab down mid-submission, which is what "cleaning up" did while the request was still in flight. ## The queue problem hiding behind the bug The bug was loud. The next failure would have been silent. My drafts are approved in batches with a 72-hour time-to-live, and the queue drains at one post per day. A batch that was approved for a 12-day runway therefore expired — all seven remaining rows — at 10:36 that same morning. The next morning's tick would have found nothing usable and simply **not posted**. Nobody gets an error message for work that no longer exists; a scheduler that runs out of work goes quiet, and quiet looks like "nothing to say today". I fixed only the expiration on the seven unused, hash-matching rows, leaving status, hash and payload untouched, because whether a draft gets re-approved is a human decision and the automation should not manufacture consent. The durable fix — sizing the approval window to the queue's drain rate instead of the batch's import date — is a decision I deliberately left open. ## What I would do differently 1. **Never `sleep(n)` and look once at anything that mounts asynchronously.** Poll for the artifact you need, with a deadline and a named failure. 2. **Treat a repeated false failure as evidence the first diagnosis was wrong.** "The session may be dead" was a story that fit the symptom and was never tested. 3. **Make the error message name the missing precondition.** Vague messages do not just fail to help — they actively misdirect, and this one sent me looking at sessions and markup for two days. 4. **Prove submission by the result, not by the click.** An input event is not a side effect; the disappearance of the composer is. 5. **Monitor the queue separately from the job.** A job that runs perfectly and posts nothing is indistinguishable from a working one unless you count what came out. ## Verification | Check | Result | |---|---| | Post live | published, with the permalink and activity URN read back from the page | | Content | authenticated read-back confirms the body text and the bold phrase rendered correctly | | Cron path | re-ran the job: `ok`, `failure_streak` back to 0 | | Double-post guard | correctly refused a second post — "post cap reached: 1 done today" | | Ledger | approval consumed and a `POST_PUBLISHED` row written with the permalink | | Tests | 404 pass, including the 13 new ones for the staged mount and the swallowed click | | Cleanup | the 11 tabs my probes opened were closed; the browser's original 4 were left as found | The post that went out, by the way, was the one about running speech-to-text on my own hardware — [the two-of-everything setup I described here](/posts/self-hosted-speech-to-text-api/). It has nothing to do with this bug. That is rather the point: the job's reporting was wrong about what had happened, and wrong reporting is worse than no reporting. ## Want this for your business? If you have a repetitive publishing, reporting or data-entry job that a person is doing by hand — or an automation that "sometimes fails" and nobody knows why — I build and debug browser and API automations on infrastructure I run myself. Most of the work in a post like this is not the happy path; it is making the failure honest enough to fix. **WhatsApp: [+60 12-797 2969](https://wa.me/60127972969)** · **Email: [me@hoelee.com](mailto:me@hoelee.com?subject=Automation%20that%20tells%20the%20truth)** · **[hoelee.com](https://hoelee.com)** I run Hoelee Enterprise, a website design and development business for Malaysian SMEs — and the automation work above runs on the same infrastructure I build for clients. If you are hiring for a platform, DevOps or AI-infrastructure role, the same channels reach me.